Skip to content
This repository has been archived by the owner on Mar 31, 2018. It is now read-only.

Why promises in the core at all? #21

Closed
balupton opened this issue Feb 16, 2016 · 25 comments
Closed

Why promises in the core at all? #21

balupton opened this issue Feb 16, 2016 · 25 comments

Comments

@balupton
Copy link

A topic to consolidate information on why the PR is even being considered.

Pro Arguments

Pros of Promises in the Core

  • will improve ease of using node by providing an official path for easier async than callbacks, e.g. Promise.all and the upcoming await
  • could eventually lead to easier debugging, although may not actually be the case now - see post-mortem discussions
  • less fragmentation in the promise community Why promises in the core at all? #21 (comment)
  • less fragmentation between node and the javascript scene in general as it evolves like it is doing in browsers (where they have already embraced promises) Why promises in the core at all? #21 (comment)

Pros of Promises in General

  • await is dependent on promises

Con Arguments

Cons of Promises in the Core

Cons of Promises in General

  • ...

More details.

Feel free to update with additional resources and if you still don't like the PR add to the discussion here.

@balupton balupton mentioned this issue Feb 16, 2016
7 tasks
@petkaantonov
Copy link
Contributor

I'd add that another argument against having promises in core is that it could require compromises that will make the situation actually worse than if core didn't simply do anything.

Another argument for having promises is that the web platform has fully embraced promises and node not doing that would cause the platforms to eventually become too different. Implicit here is the assumption that a substantial contributor to node's success is the accessibility to web developers and similarity with web development. While technically both will be using "javascript" they will be so different that they might as well be different languages.

@chrisdickinson
Copy link
Contributor

I'd add that official support reduces friction in the Node ecosystem by removing the need for two package authors who both want to use promises to specify the specific shim, implementation, and version of promises they want to use before interoperating.

@benjamingr
Copy link
Member

It's also important to note promises are the language standard way to perform asynchronous computation. I understand that that does not make them the node standard way to do so - but getting consensus by TC39 is definitely not something easy - and we can dig a lot of discussion about why promises in JavaScript. Adoption by the DOM is also a plus IMO.

As for against arguments - the fact postponing the discussion a year would let us observe how things like cancellation, async/await and others fan out. We're implementing a feature (promises in core) related to a feature (async/await) that is not yet standardized completely - if the standard changes (unlikely, but possible) we'll all eat our hats.

@rvagg
Copy link
Member

rvagg commented Feb 17, 2016

@benjamingr do you know if we have any records of the discussions at TC39 regarding their embrace? That would probably be helpful to look at if it's available.

@medikoo
Copy link

medikoo commented Feb 17, 2016

Node.js currently stands on simple async functions that take callbacks, so probably it would be good to define first what's the difference between callback functions (in Node.js async function sense), and promises.

In my eyes:

Callback function is simplest possible low-level API for handling asynchronous calls, which turns to be difficult to use and maintain when used directly to deal with complex async flows.

Promise is more high-level API for handling asynchronous calls, which when speaking of single async call brings more clutter than callback function, but it clearly stands out when we deal with complex async flows (and already was marked as standard for that).

I'd say callback function API as a low-level one should never be compromised, however as in most cases when we work with Node.js we deal with complex async flows and want to use promises, a native promises support (as an addon) should be considered.


The other and probably more blocking problem is that while we have Promise In ES standard, it is a controversial version, which is followed with controversial implementations, and many (including myself) do not look forward to rely on it.

There are different promise libraries that explore different approaches, and devs should be able to use Node.js with any of them.

So if native promise support in Node.js cannot come with flexibility where developer can choose which promise implementation is used behind, then it probably should not land at all.

@benjamingr
Copy link
Member

@rvagg I'll look into it.

I'm also concerned about being ready for async/await as well as other features like observables and async iterators. I think we should make sure we're involved in these discussions before the constructs they represent are in the spec.

Promises were centralized in https://github.com/domenic/promises-unwrapping and meeting notes.

https://github.com/domenic/promises-unwrapping contains some useful resources like:

Stuff like why .resolve and in ESDiscuss here.

Removing .cast for example and in ESDiscuss here. Also discusses unwrapping semantics.

About .done being removed.

About having the promise constructor wrap, rather than use a deferred.

As for the actual decision to include promises: there is https://esdiscuss.org/topic/a-challenge-problem-for-promise-designers-was-re-futures and https://esdiscuss.org/topic/promises-final-steps and https://esdiscuss.org/topic/are-promises-and-microtasks-introduced-into-es6 and https://esdiscuss.org/topic/parameter-to-promise-constructor and https://esdiscuss.org/topic/ecmascript-error-sink-was-weak-callbacks and meeting notes here https://github.com/rwaldron/tc39-notes/blob/244797871dfa8fd475fd593821605fee14d2cb05/es6/2014-01/jan-30.md and https://github.com/rwaldron/tc39-notes/blob/244797871dfa8fd475fd593821605fee14d2cb05/es6/2014-04/apr-8.md and https://github.com/rwaldron/tc39-notes/blob/244797871dfa8fd475fd593821605fee14d2cb05/es6/2013-09/sept-19.md (the last touches on promise adoption in the spec) and

@benjamingr
Copy link
Member

@medikoo first of all thanks for stopping by! We'd love to have you participate in the discussions in this repo.

Callback function is simplest possible low-level API for handling asynchronous calls, which turns to be difficult to use and maintain when used directly to deal with complex async flows.

This is a very opinionated take. Many believe that it is not difficult to deal with callbacks, maintain code with them or deal directly with complex async flows with helper libraries like async.

Promise is more high-level API for handling asynchronous calls, which when speaking of single async call brings more clutter than callback function, but it clearly stands out when we deal with complex async flows (and already was marked as standard for that).

I'm not sure promises are inherently higher level than callbacks. That's certainly not the case in some other languages. Also, whether or not it stands out when dealing with async flows is debatable and the goal of this repo is not to debate promises vs. callbacks it's about making lives easier for promise and callback users in Node in the light of promises.

I'd say callback function API as a low-level one should never be compromised, however as in most cases when we work with Node.js we deal with complex async flows and want to use promises, a native promises support (as an addon) should be considered.

The callback API is never going anywhere - it would literally break every single node program :)

The goal of this proposal is to add promise support, we probably won't make an argument about how promises simplify things but rather only aim to support both user bases reasonably.

The other and probably more blocking problem is that while we have Promise In ES standard, it is a controversial version, which is followed with controversial implementations, and many (including myself) do not look forward to rely on it.

The things you find controversial are not what others fine controversial and vice versa. Lots of people are looking forward to rely on it. That said - no one is taking the ability to roll your own control flow anywhere. That is not even being considered. Again - the existing APIs are mostly frozen and no one is suggesting moving them anywhere.

@chrisdickinson
Copy link
Contributor

As for against arguments - the fact postponing the discussion a year would let us observe how things like cancellation, async/await and others fan out. We're implementing a feature (promises in core) related to a feature (async/await) that is not yet standardized completely - if the standard changes (unlikely, but possible) we'll all eat our hats.

With regards to async/await, I believe the userland use of the syntax through transpilation anchors the outcome a bit — it seems unlikely that they'll base the syntax on another, non-Promise, pattern.

However, cancelation is a concern, depending on how it's implemented (at some point I saw discussion of cancelable promises being a subclass, which would be bad for our purposes.) What is the current state of discussion on cancelable promises? Are you concerned that they will move towards basing async/await on CancellablePromises? Given the long timeline on unflagging the initial PR, do you think we could use the promise-based code in the Node API as a platform for advocating for or against changes in that realm?

@benjamingr
Copy link
Member

@chrisdickinson well, I don't want to sound like the broken record but that one guy working on that won't really be interested in our discussions at this point - so I can share what limited info I have:

  • The initial async/await implementation will not be blocked on cancellation. Neither should we.
  • Subsequent standards impact async/await behavior. Probably by running finally blocks and aborting code in the middle - kind of like a generator's return.
  • Cancellation has not yet reached a consensus.
  • I definitely think Node should be involved in the discussion of cancellation semantics and if we have any meaningful input by then of async/await semantics. Whatever they decide Node will have to live with. They're making their decisions very slowly and very carefully so we'll likely have time.

@chrisdickinson
Copy link
Contributor

@benjamingr:

I definitely think Node should be involved in the discussion of cancellation semantics and if we have any meaningful input by then of async/await semantics. Whatever they decide Node will have to live with. They're making their decisions very slowly and very carefully so we'll likely have time.

Excellent, agreed. How do interested folks get involved?

Subsequent standards impact async/await behavior. Probably by running finally blocks and aborting code in the middle - kind of like a generator's return.

Hm, but wouldn't it have to be the case that async/await would re-resolve promises passed to await as "cancelable" at that point? If that's the case, it doesn't seem cancelability (as it relates to promises) would affect our design decisions in the immediate future, since very few of the operations we're exposing can be meaningfully canceled once queued.

@benjamingr
Copy link
Member

If that's the case, it doesn't seem cancelability (as it relates to promises) would affect our design decisions in the immediate future.

I tend to agree. It does however affect our ability to add the error recovery object parameter.

since very few of the operations we're exposing can be meaningfully canceled once queued.

Well, http requests, socket writes, queued file operations and so on.

@winterland1989
Copy link

About several months ago, i sent a mail to es-discuss to discuss about the possibility to reconsider promise as an Async primitive. The thread are getting rather furious, without getting any meaningful result, now i suppose this is the right place to discuss this matter? And i will keep as humble as possible in order to conduct my thought.

A well-specified pattern that provides a container type designed to describe the dependence graph of asynchronous operations within a program. As a container type, any value T may be cast to an asynchronous value Promise. Any value Promise may be unwrapped to T by passing a handler (defined below) to .then. Unwrapping via .then creates a new Promise that will resolve to the return value of the handler. One promise may have zero-to-many child promises.

From what i understand, Promise is a proxy to a T typed value, which is produced in future or past.
Promise use a task queue to achieve this:

  • When this T value is not available yet, any callback which need this T value will be enqueued.
  • When this T value are produced by an async action, all enqueued callbacks will be feed with this value.
  • After T value are produced, any further callback will get this value directly.

So each Promise is a micro single time event queue, and the core of Promise is to focus on the state of value, to deal with async Error, a rejected state are invented besides resolved and pending.

But Promise is not the only way to describe the dependence graph of asynchronous operations , Promise get widely accepted because its monodic interface, e.g. then.

What i want to present here, is to use classical continuation construction to solve callback problem, which is widely used as control structures in other functional programming languages, see ConT/haskell, Continuation Monad/clojure. We even have a concrete proposal Composition Functions for ES.next, which i discovered after i roll my own implementation. Allow me to describe what continuation is, and why i think it's a better async primitive.

A continuation is a function which consume a callback to give its value, what it abstract is not the value itself, but the action to produce this value. for example:

(cb) => fs.readFile('...', (err, data) => cb(data))

is a continuation, it wait a callback to produce a value, if we want to add a monadic interface, we can just wrap it in an instance, let call it an Action(the name is not important):

function Action(cont){
  this.cont = cont;
}

Now add a monadic compose is easy:

Action.prototype.fmap = function(cb){
  return new Action(
    (_cb) => this._go((data) => _cb(cb(data)))
  )
}
Action.prototype.mbind = function(cb){
  return new Action(
    (_cb) => this._go((data) => cb(data).cont(_cb))
  )
}

In fact it's a direct translation from haskell's ConT monad's fmap and bind:

instance Functor (ContT r m) where
    fmap f m = ContT $ \ c -> runContT m (c . f)

instance Monad (ContT r m) where
    m >>= k  = ContT $ \ c -> runContT m (\ x -> runContT (k x) c)

And in my implementation i use instanceof to dynamicly choose fmap or mbind depend on what cb(data) return, it makes sense since javascript is a dynamic language.

Why this abstract important? because it capture the essence of continuation composition, and you can build any error handling scheme on top of it. While promise is a proxy to its value, Action is a wrapper to its underline action which produce this value. This will bring a whole possibilities to do retry/throttle/parallel/sequenece execution.

If we add Promise into core, it means attaching a macro callback queue to every async value become a standard way, and accept its error handling scheme, i will strongly against this heavy weight solution. After all, even transformers package are not in haskell's base library.

@omsmith
Copy link

omsmith commented Feb 18, 2016

@winterland1989 Hey winterland - thank you for voicing your opinion.

i suppose this is the right place to discuss this matter

Unfortunately, probably not. This is about providing Promise returning APIs in the standard installation of Node.js. es-discuss would be the correct place to drive ES language decisions (to the best of my knowledge).

If we add Promise into core, it means attaching a macro callback queue to every async value

No (popular) proposal for tighter integration of Promises in Node.js core involves touching the current APIs. All of the APIs in place today will still be available, and still accept callbacks.

@balupton
Copy link
Author

@rvagg had some good points on this threads question at #10 (comment) which are applicable here.

Notably, it needs to be noted which for and against arguments are "promises in core" or just "promises in general" and to not conflate the two, as it does seem that some of the pros and cons already listed can equally be handled in user-land. User-land pros should not necessarily justify core changes. Core changes should be justified by arguments "better or worse in the core" or "must be or shouldn't in core". However, documenting pros and cons of promises in general is still valuable as they will still help influence and come up with more specific pros and con arguments for the core.

@benjamingr
Copy link
Member

@winterland1989 not at all, this is not the place to discuss these issues - esdiscuss is. Typically - they don't make up standard either - they check what solutions the community has already widely adopted (like promises - again - 20m downloads for node) and then discuss including them in the language.

If you'd like to propose an additional primitive, first you'll have to actually write a userland solution and make sure people find it ergonomic and actually use it.

(Also, of course a promise is a particular instance of a continuation with caching and error handling - that's the point)

@winterland1989
Copy link

not at all, this is not the place to discuss these issues - esdiscuss is. Typically - they don't make up standard either - they check what solutions the community has already widely adopted (like promises - again - 20m downloads for node) and then discuss including them in the language.

Ok, i understand it, so i leave my message as this:

I think we should not make Promise into node's core because it's an opinionated solution to async and async error handling. It bundled with a error-handling scheme which not only affect origin throw semantics, but also have its own problem.

Leave Promise as userland library will keep core minimal, which is a very good criteria.

Also, of course a promise is a particular instance of a continuation with caching and error handling - that's the point)

It's a particular instance of EvenEmmiter, not a continuation, a continuation is about a action which happen in future, not the value it produced. It happen to have a monadic interface doesn't mean it's a monad.

@winterland1989
Copy link

If you'd like to propose an additional primitive, first you'll have to actually write a userland solution and make sure people find it ergonomic and actually use it.

I do actually write a user land solution, see action-js, we use in our own company, i know its adoption is very low, but get larger adoption is another problem isn't it : )

@benjamingr
Copy link
Member

It's a particular instance of EvenEmmiter, not a continuation, a continuation is about a action which happen in future, not the value it produced. It happen to have a monadic interface doesn't mean it's a monad.

http://blog.sigfpe.com/2008/12/mother-of-all-monads.html - although if you really want to have that argument go have it with headinthebox :P

Of course, whether something is actually a monad or isn't (yours isn't technically either because it has a wrong signature for flatMap, like promises) is irrelevant here.

I think we should not make Promise into node's core because it's an opinionated solution to async and async error handling.

Yes, just like callbacks are an opinionated solution to async and async error handling. We're discussing adding promises to core because they're in the language standard and the biggest host environment (the DOM) has adopted them.

I do actually write a user land solution, see action-js, we use in our own company, i know its adoption is very low, but get larger adoption is another problem isn't it : )

Well, actually I think it's the same problem - the language technical committee and node core both have an extremely high standard for inclusion. When we tried back in the 2000s to build standard based on what technical committees like and not what users wanted and found popular things didn't work out too well (for example - xhtml) and the community divided.

The way to the standard today (or core) is to prove two things:

  • That the current userland solutions have been widely adopted and endorsed by the community and have been battle tested.
  • That the current userland solutions don't solve the problem better than solutions in the core/langauge.
  • That the community has shown a need for these solutions in the core/language.

The reason is that different people have a very different opinion and perspective. The only way to move forward in what probably is the largest ecosystem in the world (JavaScript and Web Dev) is to adopt community and userland solutions. We used to be able to do that for library features (for example Array.prototype.map ) but now with babel we can do that for language features too.

Also, it's not just "who has the best idea" it's also "who can put in the time to document it, test it, explain it and show its need.

Hope that helps. In any case - I suggest we don't keep talking about your library here but you're welcome to bring it up in the appropriate channels like esdiscuss or better yet - build a userbase first :)

@winterland1989
Copy link

http://blog.sigfpe.com/2008/12/mother-of-all-monads.html - although if you really want to have that argument go have it with headinthebox :P

I don't see any relation between Promise and this article, can you elaborate?

Hope that helps. In any case - I suggest we don't keep talking about your library here but you're welcome to bring it up in the appropriate channels like esdiscuss or better yet - build a userbase first :)

Fair enough, but at least let me give my attitude to the problem Why promises in the core at all? since that's the title of this thread, i just want to emphasis these again:

  • Promise is bundled with a error-handling scheme which affect origin throw semantics, becoming language standard doesn't change the fact that this opinionated scheme cause problems.
  • Leave Promise as userland library will keep core minimal, which is a very good criteria.

@spion
Copy link

spion commented Feb 18, 2016

@winterland1989 Promises are bundled with error-handling scheme for a good reason. Its because the language is bundled with that same error handling scheme.

Callbacks and Action.js also come bundled with an error handling scheme. Its called "crash your process on every exception" and "code by pretending exceptions don't exist". This causes denial of service problems and loss of user work or data.

Coding without exceptions can be done. It will require changing all synchronous functions to make them return Result<T> instead of throwing exceptions. Like in Haskell, where you'd like to replace the unsafe prelude with a safe one that e.g. has head :: [a] -> Maybe a not head :: [a] -> a.

Its not like in Haskell its considered acceptable for head to throw because "you should've checked the length of the list first!" Instead, those who advocate avoiding exceptions also advocate replacing the prelude.

Analogously, async functions would then always return Action<T> instead of throwing. It would also be convenient if it were easy to lift Result<T> to Action<T>, perhaps automatically.

Oh and to get the same benefits as Haskell we would also need the ability to pattern match over Result<T> and get errors on non-exhaustive pattern matches. Otherwise we're back to "you should've checked the value first!" or we'd have to accept the performance drop of passing functions to emulate pattern matching.

The easier path, at least for the language was to keep exceptions (somewhat flawed as they are) and provide an async analog. I do believe that it will be unwise for ES7 to not improve exceptions by at least adding first class catch filters - this will get rid of so many issues, perhaps even post-mortem problems (stack doesn't unwind if the filters let the exception through)

@winterland1989
Copy link

Callbacks and Action.js also come bundled with an error handling scheme. Its called "crash your process on every exception" and "code by pretending exceptions don't exist". This causes denial of service problems and loss of user work or data.

I can't follow you here, Let's use haskell as example, in haskell we use an algebra type, such as Maybe a or Either err a to present a value may not exist. In javascript we don't have algebra types, the best we can get is:

  • pass err and data parameter to denote the the result maybe a failure.
  • pass a data parameter, it may be a instance of Error or a normal value.

In haskell there's no ReferenceError or TypeError ... They are statically checked, we have type level nature number to encode head function, it's here, and that's why we avoid partial functions from prelude. In haskell these errors are statically avoidable.

But in javascript these errors are not avoidable, that's why sometime you want distinguish Programmer Errors from Operational Errors, Promise should never deal with Programmer Errors, but it does so. Is adding implicity try catch a necessary?

Analogously, async functions would then always return Action instead of throwing. It would also be convenient if it were easy to lift Result to Action, perhaps automatically.

Analogously, we should at least emulate algebra types by using T :: Error | OtherType, that's what Action.js do : )

Oh and to get the same benefits as Haskell we would also need the ability to pattern match over Result and get errors on non-exhaustive pattern matches. Otherwise we're back to "you should've checked the value first!" or we'd have to accept the performance drop of passing functions to emulate pattern matching.

In fact, Actions do provide this functionality, guard function can receive extra parameter.

@winterland1989
Copy link

We can move this discussion into some place else, AFAICT this thread is not going to be the best place to discuss other async primitive, you're welcomed to open any issue at Action.js repo if you want to. But keep that in mind. Action is not something emulating Promise, it's a different thing at all. What it abstract is not the value :: T, but the continuation :: ( T -> r ) -> r .

@benjamingr
Copy link
Member

@winterland1989 @spion is well aware of that, that's what he was saying. Let's stop this discussions here - further off topic discussion will be moderated away.

@jamen
Copy link

jamen commented Feb 19, 2016

As far as fragmentation goes: I don't see how adding promises into the core would remedy that for the Node community (or how it is a pro and not a con), but rather make it worse... From my understanding, moving something out of userland and into the core doesn't remove fragmentation, providing a consistent API in the core removes fragmentation (at least on Node's end, there will always be fragmentation in userland). So, with that in mind, it would actually add fragmentation.

But then again, what is so bad about fragmentation in this area anyways? (Not rhetorical, actually curious) I think this comment touches on that, at least in userland. But, again, I don't think moving it into the core would solve that, because those userland libraries still provide a promise API, like Node would implement, but with more methods (which causes the fragmentation), and I see no reason why that would just stop and end fragmentation.

I use promises quite often, and for a while I was quite excited for them to be added into the core, but after reading the discussions here, I understand the ideology of those who don't want them in the core (which is why I thought maybe I should make a comment), and now, I somewhat agree. Fragmentation is inevitable now because of userland, bloating the core with another option doesn't solve that. I'm happy with using userland promises (and I'd like to know why other promise users aren't too).

Of course, fragmentation isn't the only pushing factor, I just thought I would touch on that. (As well as get some answers to what I'm curious about.)

I'm not too knowledgeable when it comes to this type of stuff, but I would still like to contribute to the conversation somehow (keep in mind, I'm pretty young, 14, with little experience on discussions like these. I thought participating in this conversation might be a good for learning, and its fun)

@chrisdickinson
Copy link
Contributor

@jamen Thanks for commenting! I think "fragmentation" (as @balupton uses it in the issue text) might be a bit of a loaded term — it may be more accurate to say the API provides a default, "known good" path for promise users to write packages, reduces the need for alternative implementations to maintain shim libraries, and allows users to "opt in" to choosing alternative implementations (vs. the current situation, where the decision is front-loaded.) This PR aims to gradually smooth interoperation between promise-using packages in this fashion — it won't happen all at once, but I believe the combination of providing an API and documenting best practices for interoperation with alternative implementations in core will make the situation for users much better in the long run.

With regards to fragmentation between promises and callbacks, at a code level that's not addressed in the current PR, though we can start to bridge that by exposing promisify and callbackify methods from core (with docs on how and when to use them.) This is a topic that should probably be figured out before unflagging.

At a community level, providing APIs for both callback and promise users sends the message that both are equally valid ways to build a program, and preference towards one or the other is equally valid. Promises and callbacks thus far have been treated like a zero-sum game: for promise users to make gains, callback users have to lose, or vice versa. By including the API, core asserts that it isn't a zero sum game — promise users and callback users can both be catered to, without either side losing ground. I believe this would go a long way towards cooling the temperature of the promises vs. callbacks conversation.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants