Adding promise-aware function composition.#524
Conversation
ramda.js
Outdated
There was a problem hiding this comment.
then should at least be a function
Also this is the kind of type juggling that is kinda against this libs beliefs
|
I really like this, but if I had a vote it would be to leave internal compose as it is and put this in a (newly created, with the idea that more things could be added) promise module. |
|
@kedashoe I don't think there is going to be much to add here. And I think promises are moving towards being a first class citizen in JS, so it makes sense to treat them as a part of the standard use case, rather than a special case. @megawac Good point on checking that .then is a function. I'll update the PR. |
ramda.js
Outdated
There was a problem hiding this comment.
typeof (value.then === 'function'); is boolean--the parentheses are getting in the way
There was a problem hiding this comment.
Oops, misplaced parentheses.
|
@megawac is right: Ramda generally avoids this sort of sniffing. I very much agree with the goal of this pull request, but I wish there were a less hacky way of detecting promises. |
|
Hmm indeed. Would this break if my then method isn't a promise ? |
|
I like the idea of wrapping the promise-returning function, @fyyyyy, but I don't see how it would work. var promiseSentinel = {};
R.promise = function promise(fn) {
var wrapper = arity(fn.length, function() {
return fn.apply(this, arguments);
});
wrapper.ramdaPromise = promiseSentinel;
return wrapper;
};We could then test whether the value of function's |
|
@davidchambers: I'm pretty sure that even by the spec, there is no better way to recognize promises. (Update: This was not in response to your "sentinel" comment, but to the previous one.) |
|
@davidchambers Probably i don't understand how compose works, so what i had in mind was that R.promise would keep it wrapped, and the user has to check himself if he wants the promise (via .then) or the composed function in the end. So R.compose would return a wrapper when R.promise was used somewhere. Probably silly 😄 |
|
@davidchambers R.promise would add quite a bit of boilerplate, to the point where I think an externally supplied implementation of compose would be more attractive for someone who is looking to work with promises. If you feel that relying on existence of .then is too invasive, the better alternative would be to leave R.compose() as is and add a new promise-aware function R.pcompose(). But again, for better or worse, the spec's definition of thennables is: “thenable” is an object or function that defines a then method. So, the method that, say, Q, uses for detecting promises is this: function isPromiseAlike(object) {
return isObject(object) && typeof object.then === "function";
}(I can switch to doing the same in my check.) AngularJS uses essentially the same method. So, code that makes use of objects with .then() that are not actually promises is going to have all sorts of trouble anyway. @fyyyyy I don't think there are "more types of promises" to anticipate. There are different promise libraries around, but they all ultimately trade in "thennables", which is to say objects that carry a .then() method that can be used to attach callbacks. Nearly all promise libraries out there follow this approach, which is also becoming a part of ES6 standard. So, the solution that I am proposing is not tailored for a specific kind of promises. It's just makes the basic assumption that pretty much all promise libraries make. |
|
@yuri Guess you're right that promise people would switch to a different compose when they are forced to wrap every call. Could one build it's own pCompose by doing var pCompose = R.promisable(R.compose), or R.promisable(R.pipe), or R.promisable(R.converge), and then use those throughout code? Otherwise just ignore my comment 😄 👍 |
|
Although I definitely want this behavior, I'm still not sure whether I'd prefer to have a single compose that handles anything or one dedicated to handle the possibility of Promises as well as our standard one that simply deals with synchronous functions. What I'd really like is some performance benchmarks. If dealing with the possibilities of Promises slows down one of ours most important functions, |
I also want the behavior, but IMO blending it in makes |
|
@CrossEye With a couple of optimizations, I get about 50% performance penalty, but only for composing absolutely most trivial functions, such as a(x) { return x+'A'; }, since in this case the trivial cost of running each composed function is comparable to the trivial cost of running the check presence of .then(). For functions that do non trivial stuff, however, the difference is smaller. And for trivial functions, we are talking about a difference of 100 ms over 1 million compositions. |
|
@buzzdecafe So, would you prefer that I change the PR to make this a new function, R.pcompose() or R.composeAsync() or something similar? |
|
@yuri: That's probably not too bad for numbers. But I'm still not convinced that it belongs together with the standard Ramda generally does not like to combine multiple behaviors into a single function. We do make an exception for the dynamic dispatch of a number of functions, and this feels something like that, but I'm especially bothered by the same sorts of issues @buzzdecafe raises. I'm also bothered by the almost-but-not-quite-Functor style of Promises. But there's really nothing we can do about that. One thing is certain. I want to add a dynamically dispatched |
|
@yuri, I'd like to hear from others, but that's the way I'm leaning. |
|
yes, the "it's almost a functor" thing is tantalizing but frustrating. put me down for 👍 for |
👍 |
|
Sounds good, |
|
@CrossEye @buzzdecafe I updated the PR but pcompose() examples require a promise library and I couldn't get exampleRunner to include Q for some reason. Can you suggest a way of doing that? |
|
interesting
|
|
@fyyyyy You can do this with ES6 generators, though. If it's going to require transpiling, might as well transpile ES6. |
|
@yuri thx i will check it out |
|
@fyyyyy, @yuri: This could also be done with sweet.js or in various other transpiling techniques. But none of that helps us much with our current problem. @yuri, I will try to look into this tonight. Have you actually included |
I submitted #530 so you can do this. I tested by adding
to the beginning of your example for @contributors: PR only submitted as it is a separate issue from promise composition and wasn't sure how you felt about it being included as part of this. If you don't mind, I do not mind ignoring my PR and simply having @yuri add the 1 line in example runner to his commit. |
|
@kedashoe Thanks. And damn it, I was so close! I'll add this one line to my PR. |
|
I'm not saying these names are better, but let's at least consider other options:
I actually like the names |
|
@davidchambers Either of those options look good to me. Another option would be |
|
i think |
👍 And I believe according to ramda style |
|
@buzzdecafe Maybe @kedashoe Good point on underscores. Not sure about |
Thank you for helping to enforce this style, @kedashoe. :) |
Actually in regards to this, possibly https://github.com/ramda/ramda/pull/534/files can provide some guidance.. maybe |
|
|
|
bach.js |
|
|
|
Anyone has any thoughts on |
|
aw, we gotta get back on topic?! I guess I would go with @davidchambers and @kedashoe |
|
👍 on |
R.pCompose works like R.compose, but handles promises. The resulting function will evaluate to a promise if any one of the composed functions returns a promise. If all of the composed functions return something other than a promise, then the behaviour is identical to that of R.compose(). In this sense, R.pCompose() is backwards compatible with R.compose. R.pPipe is like R.pCompose but takes the arguments in the opposite order.
|
@buzzdecafe Squashed, should be good to go. |
|
Nicely done! Thanks for all the hard work, and for sticking with it. |
Changing _compose() to support promises.
|
Thanks everyone, it was great fun! |
|
Thanks, @yuri! |
|
can this be compared to / combined with promises+generators for "piping" async function in ES6? e.g. see http://taskjs.org/ for an analogous code snippet defining an "asynchronous pipe". From the surface, the |
|
@dannyko: I am not quite sure in what way taskjs is similar. It seems to try to fix a rather different problem. But in terms of using generators in an "await"-like manner, R.pipeP should work fine for that. I mean, you should be able to just "yield" the return value of a function that was composed using R.pipeP. |
|
thanks! to me, they are both similar in that they both aim to make promises easier to combine, in particular to create a "chain" (or pipe) of evaluations that blocks on promises. Perhaps I will better appreciate the differences once I try using them at some point :) |
Compose would now handle promises returned by any of the composed functions. The resulting function
will evaluate to a promise if any one of the composed functions returns a promise. If all of the composed functions return something other than a promise, then the behaviour of compose() is the same as it was before. In this sense the change is fully backwards compatible.
I added one new test and modified two tests to test promises. I added Q to dev dependencies to test promise support. However, Q is not necessary at run time, and this implementation should work with any promise implementation.
See #512 for discussion.