Comments (12)
It sure is. Seems like you don't have a trampoline though - I find it very convenient that I can write infinitely recursive functions with Bluebird without blowing the stack, without even thinking about it.
Monadic actions/futures can be quite beautiful: here for example is @robotlolita 's wonderfully minimal Future library, translated to only a few lines of ES6: https://gist.github.com/spion/b433a665ca51d6c38e9f#file-3-reference-implementation-in-es6-js
(See the link below the fork for the original)
I wasn't criticising your library's design choices per-se: assuming that we had a "safe prelude" (one where throw
is never, ever used by user code to signal failures), it would likely be a better choice (I'd still want that trampoline though, as well as pattern matching for Result<T>
- closures are way too expensive for synchronous code)
But we do have exceptions in JavaScript and node. Lots of functions throw exceptions for all kinds of reasons - its not like we only have the automatic TypeError and ReferenceError for trying to call or dereference undefined. We have all kinds of functions that throw because they don't like their input.
We have a throwing, stateful JS prelude. Because of this, I don't really want to use a library that pretends exceptions don't exist. Additionally because of pervasive state mutation, I want a library that has execution order guarantees, i.e. I like that promises guarantee that any fulfilment or error handlers attached to them will run after the currently executing function finishes. Finally, JavaScript is eager strict and effectful, and so Promises are also equally eager strict and effectful: var x = syncFunction(y)
is very much equivalent to var promise = asyncFunction(y)
Promises are not meant to be monads. They are meant to fix the fact that synchronous JS constructs become unusable once you switch to async code. Thats why they catch all exceptions - to continue their propagation up the "async" stack, just as they would propagate if the stack was synchronous. Promises replace variables, then
replaces the semicolon, promise.catch
replaces catch {}
, promise.finally
replaces finally {}
. In Bluebird we went a bit further and added Promise.using
which would replace using
/with
/try-with-resources
if we had it in JS, to more easily provide resource and state cleanup guarantees. We also have .catch(predicate, handler)
to replace catch (error if predicate) { ... }
.
Promises are just JavaScript, and thats the reason why they were added to ES6 - they are a good fit.
from action.js.
First thank you for reviewing my code : ), you asked some good questions about the design, some because Action.js was designed to be more browser friendly, some for other reasons. i would like to explain it more, but i have limited knowledge on promise internal, so please take these with a grain of salt.
It sure is. Seems like you don't have a trampoline though - I find it very convenient that I can write infinitely recursive functions with Bluebird without blowing the stack, without even thinking about it.
That will involve nextTick
in node which doesn't exist in browser, some Promise library even allow you to choose nextTick
shim if you want to work in browsers, i choose it to keep code minimal. Unlimited monadic recursion in general is not widely used in strict language. But you always have a choice:
Action.repeat(-1,
Action.delay(0,
new Action((cb)=>{
console.log('This will be repeated forever without blow your stack!');
return cb();
})
)
)
Monadic actions/futures can be quite beautiful: here for example is @robotlolita 's wonderfully minimal Future library, translated to only a few lines of ES6: https://gist.github.com/spion/b433a665ca51d6c38e9f#file-3-reference-implementation-in-es6-js
I choose coffee because it's easier to setup than babel LOL, maybe it's time to switch to ES6, i don't know. But coffee happily compile everything to es3, which i want to support now.
We have a throwing, stateful JS prelude. Because of this, I don't really want to use a library that pretends exceptions don't exist. Additionally because of pervasive state mutation, I want a library that has execution order guarantees, i.e. I like that promises guarantee that any fulfilment or error handlers attached to them will run after the currently executing function finishes. Finally, JavaScript is eager and effectful, and so Promises are also equally eager and effectful: var x = syncFunction(y) is very much equivalent to var promise = asyncFunction(y)
That's actually what Promise upset me most, it replace try..catch
! in Action.js you have to catch your errors explicitly:
someAction
.next(function(data){
try{
return unsafeProcess(data);
}
catch(e){
return e;
}
})
.next(...)
.guard(...)
.go()
It's not pretending exceptions don't exist, it's just didn't change the way how try...catch
suppose to use, short-cut by Error
type is a scheme that won't affect original try...catch
semantics. If users choose to pretends exceptions don't exist, then errors will break their program as they supposed to.
We have a throwing, stateful JS prelude. Because of this, I don't really want to use a library that pretends exceptions don't exist. Additionally because of pervasive state mutation, I want a library that has execution order guarantees, i.e. I like that promises guarantee that any fulfilment or error handlers attached to them will run after the currently executing function finishes. Finally, JavaScript is eager and effectful, and so Promises are also equally eager and effectful: var x = syncFunction(y) is very much equivalent to var promise = asyncFunction(y)
That's the most convincing argument about why to use Promise so far, because its focus on value rather than actions. so basically people can use it like normal value.
But consider this, once we want to add some control structures on top of Promise, for example retry/throttle/error-recovery. we are forced to work on functions which produce Promise instead of Promise itself, and these control structures are become much expensive because we have to instantiate new Promises to make the action happen. customized control structure is always tend to favour lazy execution.
Promises are just JavaScript, and thats the reason why they were added to ES6 - they are a good fit.
If Promise is built into core, i think i will add following isomorphism to take advantage of that : )
fromPromise = (p) =>
new Action((cb) => p.then(cb, cb))
BTW, have you checked this project's wiki? It records some thoughts on async nature, and you're very welcomed to review it.
from action.js.
That's the most convincing argument about why to use Promise so far, because its focus on value rather than actions. so basically people can use it like normal value.
And so is promise.catch
(can use it like normal catch {}
) and promise.finally
(use it like normal finally {}
. It simply lifts sync JS semantics to async :)
It's not pretending exceptions don't exist, it's just didn't change the way how try...catch suppose to use, short-cut by Error type is a scheme that won't affect original try...catch semantics. If users choose to pretends exceptions don't exist, then errors will break their program as they supposed to.
The thing is, asynchronous code already changes the way exceptions work: They don't bubble any more. So if you don't change them back, you have effectively changed the intended behaviour of the language. All that promises do is make JS exceptions bubble again, from the last "semicolon" (then
) either to the next "catch block" (catch
) or "up the stack" (via the returned promise).
from action.js.
I have no trouble with the choice of coffeescript, just wanted to say that I admire the simplicity and elegance of lazy monadic actions.
from action.js.
And so is promise.catch (can use it like normal catch {}) and promise.finally (use it like normal finally {}. It simply lifts sync JS semantics to async :)
That's the deal breaker everyone talked about, some people like you enjoy it, while some people like me can't accept it ; )
I also hope that await sync syntax can be used with any async primitive with a monadic interface.
from action.js.
That's the deal breaker everyone talked about, some people like you enjoy it, while some people like me can't accept it ; )
Still, I haven't seen a very convincing argument against it. And also, the node people who say its a deal breaker still write code in node core that deliberately throws exceptions :)
Anyways, most downsides of exceptions are avoided by avoiding / being careful with shared mutable state. The few JS-specific downsides are avoidable by adding a typechecker (flowtype/typescript/closure)
I also hope that await sync syntax can be used with any async primitive with a monadic interface.
Unfortunately, no. I think async/await is a mistake, and, it being unusable with anything else except for promises is another good reason why.
from action.js.
Still, I haven't seen a very convincing argument against it. And also, the node people who say its a deal breaker still write code in node core that deliberately throws exceptions :)
I want to throw some points here, but maybe all these has been discussed before : )
- try...catch are lexical structure to deal with program accident and accident recovery. It's static structured, so interpreter can quickly decide error handling path. On the other hand, Promise's
catch
function are dynamic structures, they can be added by program dynamically, and interpreter have no idea what to do when error happens inside Promise chain, it have to wait reject event propagating to down stream to decide error handling path. - Base on above, Promise's error handling scheme make protecting program accident scene very hard, we have to unwind stack...etc.
- We also have to establish unhandled error path besides original one, and corresponding debug support, that's a duplicate of work IMHO.
- Use sum type to deal with error are very common in other functional languages, for example in haskell, we have
ExceptT
monad transformer:
newtype ExceptT e m a = ExceptT (m (Either e a))
it's a newtype
to some computation in monad m
with result type Either e a
, and its bind is basically a pattern match on the result:
instance (Monad m) => Monad (ExceptT e m) where
...
m >>= k = ExceptT $ do
a <- runExceptT m
case a of
Left e -> return (Left e)
Right x -> runExceptT (k x)
And Action.js provide next/guard
closely follow this approach:
Action.prototype.guard = function(cb) {
return new Action((_cb) =>
this._go(function(data){
if (data instanceof Error) {
var _data = cb(data);
if (_data instanceof Action) {
return _data._go(_cb);
} else {
return _cb(_data);
}
} else {
return _cb(data);
}
});
);
};
AFAICT this approach provide a nice interface while doesn't affect origin try...catch
.
Unfortunately, no. I think async/await is a mistake, and, it being unusable with anything else except for promises is another good reason why.
I complete agree with you on this matter, since then
or coroutine
are functions, they are first class composable value in javascript. on the other hand await/async
are keyword, which break this composablity. But suppose we really have this in language, i can't see a reason not to use for most of the case.
Check out my co
implementation, i suppose it can be made into same kind of keyword as await/async
. but i strongly against this approach either. All my hope is that the coming await/async
proposal will be more general.
from action.js.
AFAICT this approach provide a nice interface while doesn't affect origin
try...catch
It does so by ignoring reality. The reality is that throw
is used everywhere throughout JavaScript and node for all kinds of reasons.
I don't want to use this because if I do, my node programs will crash.
If used in a web service, when a user finds an endpoint that will crash, they can refresh 8 times quickly and kill all my workers.
When those workers crash they will at the same time destroy the work of hundreds of other users that are currently connected to them.
Suggested reading:
nodejs/node-v0.x-archive#4583
nodejs/node-v0.x-archive#5114
nodejs/node-v0.x-archive#5149
from action.js.
Let's say we haven't consider some dangerous operation which may throw:
//with Promise
...
.then((data) => dangerousFun(data))
...
.catch((err) => rescue(err)) // It doesn't consider above error
//with Action
...
.next((data) => dangerousFun(data))
...
.guard((err) => rescue(err)) // It doesn't consider above error
.go()
With Actions, yes when dangerousFun throws, it crash the process and report call stack/code position immediately, but that's intended behaviour. It just successfully report a bug IMO, and i can add a try...catch
to fix it later. This's just normal debugging workflow with or without async code. If i didn't get your point wrong, then you're suggesting even if writing normal sync code, we should not throw.
But with Promise, you never see what dangerousFun throw, it may leave your program in a unknown state without any sign. In fact if you use Promise, the issues you linked may be not found yet.
In most cases, you don't even know what kind of errors you have to catch in Promise chain, because Promises come from other's module are not in your control, i prefer program crash and report in that cases, but that's my personal opinion.
Since Promise community seems to be ok with this consumer decide what can be handled scheme, and prefer not to crash program, i would say this is just a personal taste : ) ,i just prefer not enclose every function with an implicit try...catch
.
from action.js.
With Actions, yes when dangerousFun throws, it crash the process and report call stack/code position immediately, but that's intend behaviour.
How do you deal with that being a potential denial of service attack vector?
But with Promise, you never see what dangerousFun throw
Bluebird outputs unhandled rejections to stderr by default, and additionally has long stack traces built in.
It may leave your program in a unknown state without any sign.
Only if I am careless with shared mutable state and resources. Which btw can happen with anything else too: https://gist.github.com/spion/60e852c4f0fff929b894 - having thrown errors crash the process doesn't help there much I'm afraid. The source of the problem is shared mutable state, not exceptions.
In fact in most cases, you don't even know what kind of errors you have to catch in Promise chain
Which is why you let them bubble up until you know how to handle those cases, but clean up resources on their way using finally
I prefer program crash
We cannot accept the possibility of having denial of service and one user affecting thousands of others. But we acknowledge that its not very easy to write code that doesn't leave the program in a bad state after an exception is thrown. Thats why we offer features that help with this, and we're working on others that make safe code more pleasant to write
The issue here isn't whether exceptions are broken or not: yes they are somewhat broken. The issue is that using throw
for all kinds of stuff then ignoring its existence and letting the process crash is far more broken.
from action.js.
How do you deal with that being a potential denial of service attack vector?
Just like any other language with throw
s, we use QA/Bug report/fixing work flow. Nothing changes at all. and it's not likely solved by just wrap every function in try...catch
.
Bluebird outputs unhandled rejections to stderr by default, and additionally has long stack traces built in.
In that example, it will not, because you do add a catch
like what Promise document suggest you, you just didn't expected some function will throw:
//with Promise
...
.then((data) => dangerousFun(data)) // when it throw, what happened next? no idea...
...
.catch((err) => rescue(err)) // It doesn't consider above error
BTW. this's very common in a large codebase with lot of people worked on, errors bubbled up from their Promises' try...catch
, but l have not idea how many cases i have to deal with, even searching for throw
show nothing because they don't have to use throw
to throw, you can save some key stokes and don't have to think about if this function throw, maybe you forget, maybe you don't know, but you totally screw other's error handling without any sign.
The issue is that using throw for all kinds of stuff then ignoring its existence and letting the process crash is far more broken.
Use throw warning others with caution, if they don't debugger will yell with detail, we do have QA so this is something we can accept.
from action.js.
Since the fundamental problem lies on Promise's implicit try...catch
, it's a design choice comes with price, so i dear to say it's reasonable to go with either direction, nice talk with you @spion. Closing issue : )
from action.js.
Related Issues (7)
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from action.js.