-
Notifications
You must be signed in to change notification settings - Fork 145
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
Returning Promises from Highland methods #290
Comments
Use _(itemStream)
.flatMap(function(item){ return _(doAsyncThing(item)); })
.each(function(item){ console.log(item); }); |
You can use _(itemStream)
.flatMap(function(item){ return _(doAsyncThing(item)); })
.each(function(item){ console.log(item); }); |
Snap! |
👏 |
Ha. Your answer is actually better because it explains why it works. |
Thanks for the answer(s), that was impressive synchronicity :) Anyhow, I do have two follow-up questions:
_(Promise.try ->
doThing()
.then (val) ->
doOtherThing(val)
) EDIT: Actually, a 3rd question: is there any particular reason Highland doesn't automatically wait for Promises from any kind of method, like Bluebird does? |
For question two, I guess you could do something like: _(itemStream)
.map(function(item){ return doAsyncThing(item); })
.map(_)
.each(function(item){ console.log(item); }); |
Question 3: because Highland isn't a Promise library. It can consume promises, like it can consume arrays, callbacks and event emitters, but its API is focused around transforming streamed values, not promises. It's like asking why |
@svozza I had to replace the second @quarterto While I understand that rationale, I'd argue that allowing for Promise waiting would be a good idea because:
I understand that waiting for Promises may not always be the desirable thing to do - perhaps this could be a configuration option of some sort? |
Highland streams already represent a future array of values, so it doesn't really make sense to treat promises as something special. You mentioned needing async versions of other operators besides |
It absolutely does, for interoperability. Many third-party libraries use (or can use) Promises to represent eventual values, and right now it seems unnecessarily hard/verbose to use those libraries with Highland. In an ecosystem that is heavily (and increasingly) modularized, this is a pretty big deal.
Frankly, I have no idea - I've only been using Highland for a short while now, so I haven't used most of it yet. Hence my rather generic question :) |
Honestly, I don't think wrapping all oulf your async calls in the stream constructor ( Now, we do have |
It's not just about the three characters, though:
While this could work to some degree, it suffers from the same problem as |
For what it's worth, I've monkeypatched in this behaviour using a 'plugin' written with the exposed methods from #293, and it looks something like this (CoffeeScript): module.exports = (_) ->
_realSend = _.Stream::_send
_.Stream::_send = (err, item) ->
if item? and item != nil and item.then?
_realSend.call(this, err, _(item))
else
_realSend.call(this, err, item) I'd imagine that this kind of functionality is not always desired, so a real-world implementation would probably look a bit different. |
This may be where we differ, but promises only "just work" in the context of the monad. The moment you stop using it with other promises, you lose the "just work". There's no expectation that it'll have the same behavior when used with other data structures. This was more or less @quarterto's point. For operations like Now, you can argue that highland should coerce to a stream whenever we expect one, like for
I don't understand. Why would you need to wrap anonymous functions? If you can wrap an anonymous function, can't you wrap its return value instead?
Can't you promisify the db object? |
I came into this convo after running into the same desire -- a way to "depromise" a stream. I can wrap the result in an I too think there should be a way to resolve promises more gracefully in the stream despite the reasons against it. :) Perhaps a |
There is a way to resolve promises gracefully in Highland. It just doesn't happen automatically. I can see adding // Same as s.flatReject(predicate)
s.flatFilter(_.seq(predicate, _.map(_.not))) You can even extract the transform for reuse if you want. const flatReject = predicate => _.flatFilter(_.seq(predicate, _.map(_.not)));
s.through(flatReject(predicate));
s2.through(flatReject(anotherPredicate)); |
There's also
I don't know quite what you're talking about 😉 |
This is all very helpful as far as real-life examples and use cases. One thing I don't understand is: This works as you specified: function triple( value ) {
return Promise.resolve( value * 3 );
}
it( 'promise test', function ( done ) {
h( [1, 2, 3, 4, 5, 6, 7, 8, 9] ).map( triple ).flatMap( h ).each( h.log ).done(done);
} However changing the initial it( 'promise test', function ( done ) {
h( [1, 2, 3, 4, 5, 6, 7, 8, 9] ).flatMap( triple ).flatMap( h ).each( h.log ).done(done);
}
Error: Expected Stream, got object
at kinesis/node_modules/highland/lib/index.js:3049:26 |
The function passed to |
Thanks for that. I think in some of the discussion above, we talked about how to resolve promises, but there is a flip side to them as well. How to handle the rejects. This is where some new functions would really shine. function triple( value ) {
var result = value * 3;
if ( result % 5 === 0 ) return Promise.reject( result );
return Promise.resolve( result );
}
it( 'promise test', function ( done ) {
var a = _( [1, 2, 3, 4, 5, 6, 7, 8, 9] )
.map( triple ) // Promise: resolves to 3x, rejects if divisible by 5
.flatMap( _ ) // resolve promises
.each( _.log )
.done(done)
;
}
3
6
9
12 The test errors because the Promise.reject() stops the processing (without any exception report). Is it possible to extend highland functionality (via plugin, or alternative syntax) to achieve the following? It would be very helpful to the workflow system I am attempting. it( 'promise test', function ( done ) {
var a = _( [1, 2, 3, 4, 5, 6, 7, 8, 9] )
.map( triple ) // Promise: resolves to 3x, rejects if divisible by 5
;
var a$ = a.observe().resolve().each( _.log );
var b = a.observe().reject().each( _.log );
_([ a$, b ]).merge().done(done);
} |
Rejects are handled correctly: as stream errors. It's just that you're not catching them, so var a = _( [1, 2, 3, 4, 5, 6, 7, 8, 9] )
.map( triple ) // Promise: resolves to 3x, rejects if divisible by 5
.flatMap( _ ) // resolve promises
.errors(err => _.log('error', err)) // catch error.
.each( _.log )
.done(() => console.log('done'))
;
3
6
9
12
error 15
18
21
24
27
done
Yes. A "plugin" operator would be typically implemented as functions that returns a transform function if your operator takes in an argument. All of the top-level operators (like // This is functionally how _.map is implemented.
_.map = function (f) {
return s => s.map(f);
}
// You can implement a plugin directly.
function resolve(s) {
return s
.flatMap(_)
.errors(err => {}); // get rid of rejects.
}
function reject(s) {
return s
.flatMap(_)
.filter(x => false) // Filter out resolves.
.errors((err, push) => push(null, err)); // Push rejects as values.
}
// Or as a constructor
function observePromises(done) {
return s => {
var a$ = s.observe().through(resolve).each(_.log);
var b = s.observe().through(reject).each(_.log);
return _([a$, b]).merge().done(done);
}
}
it('promise test', function (done) {
_([1, 2, 3, 4, 5, 6, 7, 8, 9])
.map(triple)
.through(observePromises(done));
} |
Again, thanks so much for putting this documentation somewhere. The existing documentation is a good reference guide, but at least for me, it doesn't inform me as to the real world use cases we have been discussing. It is interesting that I can use I was able to get the following flow working as a single chain without // (Source) ---> Stage 1 ---> Stage 2 ---> Done
// \--> error \--> error
function double( value ) {
var result = value * 2 + 2;
if ( result % 5 === 0 ) return Promise.reject( result );
return Promise.resolve( result );
}
function log( prefix ) {
return function ( message ) {
console.log( prefix + ':' + message );
}
}
it.only( 'promise test', function ( done ) {
var a = _( [1, 2, 3, 4, 5, 6, 7, 8, 9] )
.map( double ) // Promise: resolves to 2x+2, rejects if divisible by 5
.flatMap( _ ) // resolve promises
.errors( log('stage 1 error') )
.map( double ) // Promise: resolves to 2x+2, rejects if divisible by 5
.flatMap( _ ) // resolve promises
.errors( log('stage 2 error') )
.each( log('result') )
.done(done)
;
} ); |
I'm unsure what why mean by "short-circuit"? Do you want to throw an error that immediately stops the stream and all processing? Is stopOnError what you want? |
You are correct that I want to throw an error that immediately stops the stream and all processing. However, I also want to throw errors that are handled by the Is there a way to batch errors? In my example above, if I wanted to report log 2 errors at a time, can that be done without introducing |
You mean like this? s.errors((err, push) => {
log('stage 1 error', err);
push(err); // Rethrow error.
})
... // Do other things.
.stopOnError(...); // Later on, stop on error.
.each( log('result') )
.done(done)
;
}); If you have further questions about error handling, can you ask them in a new issue? This isn't about promises anymore 😃. |
You are right, of course. I'll ask the batch question in another thread. Thanks. |
Just my 2 cents, if I'm understanding this correctly. If you had |
I'm assuming that I've talked about the reason why we don't want Highland streams are not promises and promises are not the values they contain. Why would Highland, a library that is not at all related to promises (besides being a way to manage asynchrony), need to treat promises in a special way? Furthermore, can you guarantee that there's no situation in which anybody would want a stream of promises? Because that would be the result of making Your analogy to arrays in your last sentence doesn't make sense. Highland allows you to say For an extremely contrived example, suppose I created a special wrapper type called Of course, there's no reason to ever create the **In a way, all javascript values are the sync analogue to promises. |
Another point is that if do decide to be Fantasyland compliant we won't be able to because we'll be breaking the monad laws. As an aside, there are plenty of people who consider the way |
My workflow looks somewhat like this:
... where
doAsyncThing
returns a Bluebird promise. I had expected for Highland to await resolution of the promise and pass the final resolution value to the.each
call (much like Bluebird's own.map
and.each
), but instead my.each
method is receiving the Promise objects themselves.Is it possible to make Highland behave like I'm trying to accomplish, without having to manually
.then
in the.each
handler, as that would defeat the point of doing things this way?The text was updated successfully, but these errors were encountered: