-
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
Implementing error handling as described in issue #35 #104
base: master
Are you sure you want to change the base?
Conversation
I feel this is an elegant solution to the errors problem. The core API stays the same and has the same feeling -- It also doesn't overcomplicate the dependency graph created from attaching to streams -- each Also, providing an easy way to swap out the |
|
||
### Getting data | ||
+ `s()` get the last right value or throws an exception if there is a left value | ||
+ `s.left()` get the last left value or throws an exception if there is a right 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.
How about adding an s.either()
that returns the left or right value ?
Perhaps we should change some of the API for consistency -> flyd.mapEither // replace mapAll
stream() // throws if stream.isLeft()
stream.left() // throws if stream.isRight()
stream.either() // never throws, returns the Either Also, +1 for adding |
Good suggestion on the |
var result = []; | ||
var f = function(v) { result.push(v); }; | ||
flyd.on(f, s); | ||
s(Either.Right(1))(Either.Left(-1))(Either.Right(2)); |
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.
So at the point where you push a left into the stream, is the Stream a left? If so, it should not be able to go back to being a right at any point -- it should be as if the stream has ended in a way.
I'm confused by this pull request. If I have something on a stream that predictably throws, I would store it on the stream as an This pull request as I understand it attempts to "bake in" Either logic into stream mapping, but in the process makes it less predictable. For instance, correct me if I'm wrong, but doesn't this flyd.map = curryN(2, function(f, s) {
return combine(function(s, self) {
if (isRight(s.val)) self(f(s()));
}, [s]);
}) I have not looked at it too closely, but it seems like there is a kind of "safe" mode, where you would always use This all may be fixable, but another issue I have with baking in Either logic is that in a lot of cases it isn't needed at all in streams. Sometimes it is all simply mapping pure functions all the way down. For instance, no stream in the Elm architecture needs Either logic. And if you look at their Signal implementation (surely a model for flyd), not an Either in sight. Granted, we are talking about JS and not Elm here 😫 I sense that one of the main motivations for these changes are to make up for deficiencies or ... weirdness ... in JS promises. If that's the case then why not simply (a) wrap promises to always resolve/reject to an Either as @paldepind suggested or (b) use a Task library (e.g. data.task) ? |
@ericgj When I first saw this PR (yesterday) I got excited at the idea of having Eithers baked into flyd streams but I think I'm now in your camp in that this is probably creating more problems than it's solving and upsetting the current, and very beneficial, simplicity of these streams. For me, the possible pains come when you first decide to push Eithers into your stream and want to map over the values inside them. There is just a lot of unwrapping to be done and possible side effects depending on how you want to handle the occurrence of a Left. E.g. var flyd = require('flyd');
var R = require('ramda');
var S = require('sanctuary');
var count = 0;
var stream1 = flyd.stream();
var times2 = function(x) { return x * 2; }
setInterval(function() {
stream1(S.Right([count++]));
}, 1000);
setTimeout(function() {
stream1(S.Left('Big Error!'));
}, 3000);
flyd
.scan(function(a, v) {
if (S.isLeft(a)) {
return a;
} else if (S.isLeft(v)) {
return v;
} else {
return a.concat(v);
}
}, S.Right([0]), stream1)
.map(R.map(R.map(times2))) // a lot of unpacking here
.map(console.log.bind(console));
// outputs
Right([0])
Right([0, 0])
Right([0, 0, 2])
Left("Big Error!")
Left("Big Error!") I also think that maybe for error handling, I should just push a value into the a stream's end stream and listen for that. Using Eithers for promises also seems perfectly reasonable. |
@jordalgo's main pain point was my inspiration
This pr may not be the best solution. First how about allowing the user to override how flyd handles promises. // this would be how it works now
flyd.promiseResolved = function(s){ function(result) { return s(result); } };
flyd.promiseRejected = function(s){ function(err) {} };
// change where flyd calls .then(s) to .then(flyd.promiseResolved(s), flyd.promiseRejected(s))
// If using Either you could
flyd.promiseResolved = function(s){ function(result) { return s(Either.Right(result)); } };
flyd.promiseRejected = function(s){ function(err) { return s(Either.Left(err)); } };
// If you want to warn on uncaught promises
flyd.promiseRejected = function(s){ function(err) { return console.warn('uncaught promise rejection', err); } }; To help working with Either maybe we can introduce a liftMonads function to module // just to illustrate functionality, something along the lines of this curried
function liftMonads2(fn, ma, mb){
return ma.chain(a){
return mb.chain(b){
return fn(a, b);
}
}
} Updating @jordalgo's example var flyd = require('flyd');
var liftStream = require('flyd/module/lift');
var liftMonad = require('flyd/module/liftMonad');
var liftEither = R.compose(liftMonad, liftStream);
var R = require('ramda');
var S = require('sanctuary');
var count = 0;
var stream1 = flyd.stream();
var times2 = function(x) { return x * 2; }
setInterval(function() {
stream1(S.Right([count++]));
}, 1000);
setTimeout(function() {
stream1(S.Left('Big Error!'));
}, 3000);
flyd
.scan(liftEither(function(a, b){
return a.concat(b);
}), S.Right([0]), stream1)
.map(liftEither(times2)) // less unpacking here
.map(console.log.bind(console));
// outputs
Right([0])
Right([0, 1])
Right([0, 1, 2])
Left("Big Error!")
Left("Big Error!") |
@jplikesbikes I think liftMonads as module would definitely be useful (not being part of the core lib seems risk free) 👍 As far as the promise overriding, part of me wants to remove any special handling of promises from flyd all together. The fact that it swallow rejections is not good, which is probably why I haven't used the built in promise handling yet. It's functionality that flyd shouldn't have to concern itself with. It might be a fun experiment to try implementing a flyd-like library that only emits Eithers and also try/catches on every single function (map, scan, transduce, etc...) so that if any one of those throws, the stream would stay intact but just emit a Left with the captured error. e.g. var count = 0;
var stream1 = flyd.stream();
setInterval(function() {
if (count == 3) {
stream1('str');
} else {
stream1(count++);
}
}, 1000);
stream1
.map(function(a) {
return JSON.parse(a)
})
.map(console.log.bind(console));
//Output
Right(0);
Right(1);
Right(2);
Right(3);
Left('SyntaxError: Unexpected token s(…)'); |
Ok, someone tell me what is wrong with this because I'm kinda loving it right now. Here is the modified flyd.map to return Eithers. var S = require('sanctuary');
function map(f, s) {
return combine(function(s, self) {
if (s.val instanceof S.Either) {
try {
self(s.val.map(f));
} catch (e) {
self(S.Left(e.message));
}
return;
}
self(S.encaseEither(R.prop('message'), f, s.val));
}, [s]);
} Here you can push any value into the stream and it will turn it into an Either and apply the functions safely to the wrapped value - retuning a Left with an error message if any of those applied functions fail. |
First of all, this pull request is high quality. I really appreciate the great and well thought out explanation. Talking about errors is necessary and I am very happy that several people have offered valuable input. My first reaction when reading the PR was very similar to @ericgj's. One of the powerful ideas in functional programming is that of creating specific solutions to specific problems. These can then be composed together to solve more complex problems. A constant challenge is to find the right and most useful abstractions. I'm not sure if builting But as both @jplikesbikes and @jordalgo rightly points out the API should still be convenient. I think we should explore how convenient we can make working with Let's consider @jordalgo's example. We can make the // ... unchanged
R.pipe(
flyd.scan(R.lift(R.concat)),
flyd.map(R.map(R.map(times2))), // still a lot of unpacking here
flyd.map(console.log.bind(console))
)(stream1);
// unchanged ... We still have the triple map though. I think @jplikesbikes's var liftM = R.compose(R.compose(R.join(id)), R.lift) But this problem is really an instance of the more general problem of working with a monad ( The main issue I see with the approach taken in this PR is that it builds two different cases into the same API.
I agree with this. I regret building promise handling into flyd. It seemed like a neat feature at the time and still kinda is. But seperating the functionality into a helper function would be a good idea I think. @jordalgo, your |
Agreed. This PR is awesome and thoughtful but should not be merged.
@paldepind I'm just tinkering with the idea of a flyd fork that always returns an I'm not sure that's a problem (if I'm understanding correctly) with the map. Users of this hypothetical stream lib would be instructed to write map functions on their streams that don't return Eithers because the error catching is already handled for them. However, if their map function did return an Either, it would just be wrapped inside another Either, which is totally fine; the user would just have to do more unpacking. |
I'm worried about this part most of all. I also expect more hairy name variants occur, like |
Ethos
flyd
to handle exceptions for the user -- anythrow
should result in a hard failure.flyd
handles Promise.reject)Concepts
events
left
and aright
side (like an Either)right
sideThe Api
s
is a streamSetting data s(...) is overloaded
s(value)
is the default case takes a value makes it a right and pushes it down the streams(promise)
if the promise resolves pushes a right, otherwise pushes a lefts(either)
pushes down right or left based on either.left either.rights.left(value)
sets the stream to a left ofvalue
Getting data
s()
get the last right value or throws an exception if there is a left values.left()
get the last left value or throws an exception if there is a right values.either()
get the last value out as an EitherChecking stream state
s.isLeft()
return true if the stream contains a left values.isRight()
return true if the stream contains a right valueCore functions
.map()
works only on rights and ignores lefts.mapEither()
gets all events as anEither
.combine()
and.merge()
stay the same they work on streams.ap()
works onrights
only.scan()
works onrights
only.on()
works onrights
onlyThe Either implementation
There are no additional dependencies and we have provided a minimal implementation for basic use. If you plan on using
.mapAll
we recommend overriding the methods in flyd.Either. You can use folktale/data.either for example as shown below.Other functionality
Keeping with the ethos of flyd any further functions like
.swap
or.onAll
should be implemented as modules.