Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor to classes #34

Merged
merged 34 commits into from
Oct 18, 2016
Merged

Refactor to classes #34

merged 34 commits into from
Oct 18, 2016

Conversation

Avaq
Copy link
Member

@Avaq Avaq commented Oct 17, 2016

This refactors the code from using closure capturing to using class properties. This way of working was inspired by the FunTask source and has several benefits:

Performance

  • chainRec 50% faster
  • cache 100% faster
  • race - 300% faster
  • parallel - 30% faster
  • All other functions - 10-20% faster

Introspectivity

Every type of Future can now have its own toString function associated with it. Casting a Future to String now shows something much closer to what the user originally typed.
This new introspective nature also allows for the creation of a more informative sanctuary-def type - something for another pull-request.

Other fixes

Besides refactoring, I also corrected some minor mis-behaviors:

  • The cancellation function of cached Futures no longer unsubscribes all listeners. Instead only the listeners for the specific call to fork which created the called cancel function will be unsubscribed. If there are other listeners left, the Future will keep running.
  • Fix some broken benchmarks.
  • When Future.or, Future.race, or Future.parallel settles, all remaining running Futures will be cancelled.
  • Now when a hooked Future is cancelled, its disposal Future is still executed, and immediately cancelled as well. This creates a much better chance for resources to be disposed, and allows a developer to forcefully get rid of resources in a much more logical place: The disposal cancellation function instead of the consumption cancellation function.
  • Future.recur was removed in favor of Future.chainRec

These changes are backwards-incompatible, and will be released under a new major version.

Avaq added 25 commits October 9, 2016 22:30
* Improve performance by 90% (almost twice as fast)
* Fix cancellation semantics (per-fork cancellation)
* Improve toString and inspect to show the state (similar to Promises)
* Improves performance by about 10%
* Improves toString
* Improve performance by about 10%
* Improve toString
* Improve performance by about 10%
* Improve toString
* Improve performance by about 300% overall
* Improve performance by about 3700% in the case where left
  resolves synchronously
* Improve toString
* Improve performance by 10-40%
* Improve toString
* When left resolves before right, right will be automatically cancelled
Now when a hooked Future is cancelled, its disposal Future is
still executed, and immediately cancelled as well. This creates
a much better chance for resources to be disposed, and allows
a developer to forcefully get rid of resources in a much more
logical place: The disposal cancellation function instead of
the consumpsion cancellation function.
Also improve toString and probably performance.
Impoves toString
An average increase in speed of 30% with cases ranging
between 10% and 100%
@codecov-io
Copy link

codecov-io commented Oct 17, 2016

Current coverage is 100% (diff: 100%)

No coverage report found for master at 289612b.

Powered by Codecov. Last update 289612b...ebc46b0

error$invalidArgument(`Future.${method}`, 0, `have a "${method}" method`, m);
};
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These will eventually disappear as I will depend on sanctuary-type-classes for FantasyLand dispatching, rewrite all other dispatchers to just construct their associated classes directly.


//We try to dispose gracefully.
conn.flushGracefully(err => {
if(err) return rej(err);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you writing return rej(err); to save yourself from typing { rej(err); return; }? If so, I urge you to reconsider as it makes the lambda's return value appear to be significant. I'd find this clearer:

if (err == null) {
  conn.close();
  res();
} else {
  rej(err);
}

}else{
isSync = false;
return;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this equivalent to the if/else above?

if (isSync !== true) {
  isSync = false;
  return;
}

}
}
while(!state.done){
isSync = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we have isSync :: Boolean rather than isSync :: Nullable Boolean? The latter is confusing and could lead to subtle bugs.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isSync has three states; undetermined, true and false. Perhaps I could represent them with numbers.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Numbers would be an improvement. This would be even clearer:

//    data Timing = Synchronous | Asynchronous | Undetermined

//    Synchronous :: Timing
const Synchronous = 'Synchronous';

//    Asynchronous :: Timing
const Asynchronous = 'Asynchronous';

//    Undetermined :: Timing
const Undetermined = 'Undetermined';

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's gorgeous. :)
I'm going for numbers though. I just tested and strings seem to cost performance. Same reason I mentioned below. V8 likes TinyInt.

@@ -216,6 +249,13 @@ describe('Constructors', () => {
expect(f).to.throw(error);
});

it('has custom toString and inspect', () => {
const m = Future.encase((a) => (a), 1);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why (a) => (a) rather than a => a?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy-pasting

/////////////////

function SafeFuture(computation){
this._computation = computation;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

raw -> computation 👍

return function Future$ap$cancel(){ c1(); c2() };
});
CachedFuture.prototype._drainQueue = function CachedFuture$drainQueue(){
const q = this._queue, l = q.length, s = this._state, v = this._value;
Copy link

@jordalgo jordalgo Oct 17, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is probably optimized for length, but this is another case when I think a minifier would be great so this function can be a bit easier to understand.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to name these such small names. I actually just did this to move property-access to outside of the loop.

function Future$bimap(f, g){
check$bimap(this, f, g);
CachedFuture.STATE = {
0: 'cold',

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not flip these and have the keys be the names so the code that utilizes the state can be easier to understand?

Copy link
Member Author

@Avaq Avaq Oct 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because this way the code can optimize variables for TinyInt. It's an untested assumption that this matters in performance though. I'll test and see. :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually; I'm also asserting state > 1 here and there. Assigning numbers to the state encodes order, which I can then use to determine how far through its state a CachedFuture is in one simple operation.

return Future.encase(f, undefined);
};
FutureChain.prototype.toString = function FutureChain$toString(){
return `${this._parent.toString()}.chain(${showf(this._chainer)})`;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first reaction to this convention was it's nice to see the parent string stringified but now I'm wondering about long Future chains and what usefulness that monster string would provide.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I'm also curious. I will tag this with 4.0.0-beta1 and test it in my own code to find out. Knowing that recursion would be done by a single ChainRec, I'm assuming that most chains won't be longer than about 20, but I'll have to see how manageable it is

@Avaq
Copy link
Member Author

Avaq commented Oct 18, 2016

Think this is ready to merge @davidchambers @jordalgo ? :)

}

function manual3if(a, b, c, d){
if(arguments.length === 1) return util.unaryPartial(manual3if, a);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This obviously can wait for another PR/commit, but make this switch just to be consistent?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No. The idea here is to test the performance of if vs switch. That's why the function is called manual3if ^^

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha. Yup that makes sense 😛

try{ r = this._fn() }catch(e){ rej(e); return noop }
res(r);
return noop;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be written as

try { res(this._fn()); } catch(e) { rej(e); } finally { return noop; }

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can't have the call to res reside inside the try block, as it will cause any subsequent actions triggered by calling res to also have their thrown Errors caught, causing the Future to enter the rejection branch after already being resolved... scary stuff.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah right! Maybe I'll add a unit test to try for this also. 👍

@jordalgo
Copy link

@Avaq This is looking good. Nice work!

@davidchambers
Copy link
Contributor

I ❤️ the named states. The changes look great to me!

@Avaq Avaq merged commit 89f2ed7 into master Oct 18, 2016
@Avaq Avaq deleted the av-classes branch October 19, 2016 13:37
@Avaq Avaq added this to the 4.0.0 milestone Oct 24, 2016
@Avaq
Copy link
Member Author

Avaq commented Oct 27, 2016

@jordalgo I've tagged a release candidate. You can install it with npm i -S avaq/fluture#4.0.0-rc.1. I find that in my existing projects, the toString seems very reasonable, with long chains of Futures typically being around 100 lines when cast to String. Since these strings really only appear in error messages, and are very helpful to determine which Future is saying the error, I think it's totally worth it. That said, I'm curious what you'll find.

@jordalgo
Copy link

@Avaq Pulled it down and it looks good. The toString method is actually pretty nice when I tried invoking it directly on a long chain 👍 However (and just from a few minutes of tinkering) I wasn't able to get a long toString error message when passing some bad args to methods in the chain. Do you have a sample of when this gets used in the case of an error?

@Avaq
Copy link
Member Author

Avaq commented Oct 29, 2016

const m = Future.of() //.long().chain()
Future(m)

@jordalgo
Copy link

Ok. Looks good, though still not sure how often it will get used :) This version looks good 👍

@Avaq
Copy link
Member Author

Avaq commented Oct 31, 2016

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants