Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Adding promise-aware function composition.#524

Merged
CrossEye merged 1 commit intoramda:masterfrom
yuri:master
Nov 19, 2014
Merged

Adding promise-aware function composition.#524
CrossEye merged 1 commit intoramda:masterfrom
yuri:master

Conversation

@yuri
Copy link
Contributor

@yuri yuri commented Nov 15, 2014

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.

ramda.js Outdated
Copy link
Contributor

Choose a reason for hiding this comment

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

then should at least be a function
Also this is the kind of type juggling that is kinda against this libs beliefs

@kedashoe
Copy link
Contributor

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.

@yuri
Copy link
Contributor Author

yuri commented Nov 15, 2014

@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
Copy link
Member

Choose a reason for hiding this comment

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

typeof (value.then === 'function'); is boolean--the parentheses are getting in the way

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oops, misplaced parentheses.

@davidchambers
Copy link
Member

@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.

@fyyyyy
Copy link
Contributor

fyyyyy commented Nov 16, 2014

Hmm indeed. Would this break if my then method isn't a promise ?
Could the promises be wrapped explicitly as in R.compose(fn1, R.promise(fn2), fn3)

@davidchambers
Copy link
Member

I like the idea of wrapping the promise-returning function, @fyyyyy, but I don't see how it would work. R.promise (or whatever we called it) would need to return a function which we know should return a promise. We could do something like this:

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 ramdaPromise property is promiseSentinel. This seems rather unappealing, though.

@CrossEye
Copy link
Member

@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.)

@fyyyyy
Copy link
Contributor

fyyyyy commented Nov 16, 2014

@davidchambers
Not sure either how this should work, but i like your idea neitherless. For one, it relieves implementation details from compose. If you need to support more types of promises in the future then this is the place to go to (probably?)

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 😄

@yuri
Copy link
Contributor Author

yuri commented Nov 16, 2014

@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.

@fyyyyy
Copy link
Contributor

fyyyyy commented Nov 17, 2014

@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 😄 👍

@CrossEye
Copy link
Member

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, compose, then we'd definitely have to do them separately.

@buzzdecafe
Copy link
Member

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.

I also want the behavior, but IMO blending it in makes compose more difficult to reason about. and it's not clear how it would integrate with types, e.g. what is a Maybe of a (native-js-style) Promise?

@yuri
Copy link
Contributor Author

yuri commented Nov 17, 2014

@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.

@yuri
Copy link
Contributor Author

yuri commented Nov 17, 2014

@buzzdecafe So, would you prefer that I change the PR to make this a new function, R.pcompose() or R.composeAsync() or something similar?

@CrossEye
Copy link
Member

@yuri: That's probably not too bad for numbers. But I'm still not convinced that it belongs together with the standard compose.

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 then function to Ramda. There are too many possible uses for it to not do so.

@CrossEye
Copy link
Member

@yuri, I'd like to hear from others, but that's the way I'm leaning. pcompose or pchain or something like that makes sense to me.

@buzzdecafe
Copy link
Member

yes, the "it's almost a functor" thing is tantalizing but frustrating. put me down for 👍 for pcompose

@kedashoe
Copy link
Contributor

put me down for 👍 for pcompose

👍

@yuri
Copy link
Contributor Author

yuri commented Nov 17, 2014

Sounds good, pcompose it is. I'll update the PR.

@yuri
Copy link
Contributor Author

yuri commented Nov 17, 2014

@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?

@fyyyyy
Copy link
Contributor

fyyyyy commented Nov 17, 2014

interesting
https://andreypopp.com/posts/2014-07-21-fighting-node-callbacks-with-purescript.html

This finally enables do-notation for computations on thunks, so we can write them with synchronous-looking code:

download url filename = do
contents <- getURL url
writeFile filename contents

@yuri
Copy link
Contributor Author

yuri commented Nov 17, 2014

@fyyyyy You can do this with ES6 generators, though. If it's going to require transpiling, might as well transpile ES6.

@fyyyyy
Copy link
Contributor

fyyyyy commented Nov 17, 2014

@yuri thx i will check it out

@CrossEye
Copy link
Member

@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 Q somewhere? At a quick glance, I just saw a reference in the test without an addition of Q itself. But I might have missed it.

@kedashoe
Copy link
Contributor

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?

I submitted #530 so you can do this. I tested by adding

var Q = require('q');

to the beginning of your example for _pcompose and everything seemed to go smoothly. However, worth noting the example runner is not promise aware (or more generally, asynchronous aware), so it won't actually verify any // => statements inside of a callback (I still like your example and think it is worth including Q to show what this does).

@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.

@yuri
Copy link
Contributor Author

yuri commented Nov 17, 2014

@kedashoe Thanks. And damn it, I was so close! I'll add this one line to my PR.

@davidchambers
Copy link
Member

I'm not saying these names are better, but let's at least consider other options:

  • R.pCompose and R.pPipe
  • R.composeP and R.pipeP

I actually like the names R.pcompose and R.ppipe, but they're not consistent with R.lPartial and R.rPartial.

@yuri
Copy link
Contributor Author

yuri commented Nov 18, 2014

@davidchambers Either of those options look good to me. Another option would be R.composeAsync and R.pipeAsync.

@buzzdecafe
Copy link
Member

i think async is too broad--but i could also get behind composeThen/pipeThen

@kedashoe
Copy link
Contributor

R.pCompose and R.pPipe

👍

And I believe according to ramda style isThennable should be _isThennable and makeMultiArgCompose should be _makeMultiArgCompose. Or how about just _makeComposition for the latter?

@yuri
Copy link
Contributor Author

yuri commented Nov 18, 2014

@buzzdecafe Maybe R.composeWithThen and R.pipeWithThen()?

@kedashoe Good point on underscores. Not sure about _makeComposition, but I'll go with whatever the maintainers prefer.

@davidchambers
Copy link
Member

I believe according to ramda style isThennable should be _isThennable

Thank you for helping to enforce this style, @kedashoe. :)

@kedashoe
Copy link
Contributor

Not sure about _makeComposition, but I'll go with whatever the maintainers prefer.

Actually in regards to this, possibly https://github.com/ramda/ramda/pull/534/files can provide some guidance.. maybe _createComposer in this case? But of course, go with what you like or as you say, whatever the maintainers prefer 😄

@davidchambers
Copy link
Member

_createComposer sounds good to me!

@buzzdecafe
Copy link
Member

bach.js

@CrossEye
Copy link
Member

Compose Joke

@buzzdecafe
Copy link
Member

compose(byrd, faure, cage)

@yuri
Copy link
Contributor Author

yuri commented Nov 18, 2014

Anyone has any thoughts on R.composeWithThen and R.pipeWithThen as alternative names?

@buzzdecafe
Copy link
Member

aw, we gotta get back on topic?! I guess I would go with @davidchambers and @kedashoe pCompose and pPipe for API consistency's sake

@CrossEye
Copy link
Member

👍 on pCompose and pPipe, even if that last makes me feel as though I'm stuttering.

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.
@yuri
Copy link
Contributor Author

yuri commented Nov 19, 2014

@buzzdecafe Squashed, should be good to go.

@CrossEye
Copy link
Member

Nicely done! Thanks for all the hard work, and for sticking with it.

CrossEye added a commit that referenced this pull request Nov 19, 2014
Changing _compose() to support promises.
@CrossEye CrossEye merged commit 4516a38 into ramda:master Nov 19, 2014
@yuri
Copy link
Contributor Author

yuri commented Nov 19, 2014

Thanks everyone, it was great fun!

@davidchambers
Copy link
Member

Thanks, @yuri!

@yuri yuri changed the title Changing _compose() to support promises. Adding promise-aware function composition. Nov 19, 2014
@dannyko
Copy link

dannyko commented Apr 12, 2015

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 Ramda.pipeP approach seems cleaner and simpler than the taskjs approach, but I'm not an expert so if anyone has a better understanding of the pros/cons of these two approaches, or whether they can be combined somehow, I'd be very interested to know your thoughts. Thanks

@yuri
Copy link
Contributor Author

yuri commented Apr 14, 2015

@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.

@dannyko
Copy link

dannyko commented Apr 14, 2015

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 :)

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.

9 participants