-
-
Notifications
You must be signed in to change notification settings - Fork 84
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
Conversation
* 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
But keep inspect the same
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%
Current coverage is 100% (diff: 100%)
|
error$invalidArgument(`Future.${method}`, 0, `have a "${method}" method`, m); | ||
}; | ||
} | ||
|
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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; | ||
} |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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';
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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', |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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. :)
There was a problem hiding this comment.
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)})`; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
* Use switch-statements instead of if-statements for argument length * Do recursion via the name of the function rather than its location * Move the type-check to after currying, so it doesn't happen multiple times
Also improve performance by about 5%
Think this is ready to merge @davidchambers @jordalgo ? :) |
} | ||
|
||
function manual3if(a, b, c, d){ | ||
if(arguments.length === 1) return util.unaryPartial(manual3if, a); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ^^
There was a problem hiding this comment.
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; | ||
} |
There was a problem hiding this comment.
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; }
?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. 👍
@Avaq This is looking good. Nice work! |
I ❤️ the named states. The changes look great to me! |
@jordalgo I've tagged a release candidate. You can install it with |
@Avaq Pulled it down and it looks good. The |
const m = Future.of() //.long().chain()
Future(m) |
Ok. Looks good, though still not sure how often it will get used :) This version looks good 👍 |
It's been released; https://github.com/Avaq/Fluture/releases/tag/4.0.0 |
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% fastercache
100% fasterrace
- 300% fasterparallel
- 30% fasterIntrospectivity
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:
fork
which created the calledcancel
function will be unsubscribed. If there are other listeners left, the Future will keep running.Future.or
,Future.race
, orFuture.parallel
settles, all remaining running Futures will be cancelled.Future.recur
was removed in favor ofFuture.chainRec
These changes are backwards-incompatible, and will be released under a new major version.