Code Monkey home page Code Monkey logo

Comments (12)

spion avatar spion commented on August 17, 2024

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.

winterland1989 avatar winterland1989 commented on August 17, 2024

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.

spion avatar spion commented on August 17, 2024

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.

spion avatar spion commented on August 17, 2024

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.

winterland1989 avatar winterland1989 commented on August 17, 2024

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.

spion avatar spion commented on August 17, 2024

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.

winterland1989 avatar winterland1989 commented on August 17, 2024

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.

spion avatar spion commented on August 17, 2024

@winterland1989

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.

winterland1989 avatar winterland1989 commented on August 17, 2024

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.

spion avatar spion commented on August 17, 2024

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.

winterland1989 avatar winterland1989 commented on August 17, 2024

How do you deal with that being a potential denial of service attack vector?

Just like any other language with throws, 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.

winterland1989 avatar winterland1989 commented on August 17, 2024

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 photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo 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.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.