monadic extension to do-notation

# Raphael Mu (8 years ago)

The ES Promise is an instance of Monad, a property that implies a much more concise and expressive syntax for using Promise, by exploiting its monadic properties. I've seen a lot of people complain about Promises having too clumsy a syntax, and likewise for async/await.

We now have the do-notation proposal ( strawman:do_expressions), and monadic assignment would fit well into the syntax.

The extension would allow use of <- within a do-expression for binding Promise values to variables, and the computation would behave most similarly to the Either monad in Haskell (in the following code, if promiseA or promiseB reject, the result of the entire expression would be a rejected Promise).

(monadic extension)

let finalPromise = do {
    let a <- promiseA;
    let b <- promiseB;
    let c = f(a, b);
    g(a, b, c)
}

(desugared to async/await)

let finalPromise = (async () => {
    let a = await promiseA;
    let b = await promiseB;
    let c = f(a, b);
    return g(a, b, c);
})();

(desugared to ES6)

let finalPromise = promiseA.then(a =>
    promiseB.then(b => {
        let c = f(a, b);
        return g(a, b, c);
    })
);

The do-notation would apply Promise.resolve to the last expression (like async and Promise#then), effectively returning another Promise. The current proposal doesn't specify this behavior, but there are two ways about this collision:

  1. use an explicit version of do-notation, e.g. async do { ... } or do* { ... }
  2. revise the do-notation proposal to always apply Promise.resolve to the last expression

Both choices give us a new Promise literal for free: do* { x } (1) or do { x } (2) would be functionally equivalent to Promise.resolve(x).

The idea was briefly mentioned several years ago, but didn't attract much attention ( esdiscuss/2012-March/021624).

This is an earlier draft of this proposal: edge/es

# Rick Waldron (8 years ago)

What does this do?

let finalPromise = do { let a; a <- b; }

Currently, that's an expression that means "a less than negated b"

# Kevin Smith (8 years ago)

Why not just use await within async do?

# Raphael Mu (8 years ago)

The a < -b issue could be solved by using a different operator, like </ or <#.

# Jordan Harband (8 years ago)

How is Promise an instance of Monad, if you can't ever have a Promise of a Promise?

# Mat At Bread (8 years ago)

The conflation of assignment and awaiting is a bad idea. It leads to lots of unnecessary temporary variables and makes anything but the simplest expression involving more than one await/promise very ugly - akin to the original syntax you're trying to avoid with inner variable declarations and <- operations.

A very similar syntax was used by nodent until v1 when it was replaced with async and await.

I'd be much more interested in a VM extension that performed auto boxing/unboxing on Promises, achieving a similar result without the need for additional syntactic constructions

# Brendan Eich (8 years ago)

+1 to experience report from nodent, and +many to futures or eventual values. We've discussed before in terms of value types, value proxies, "become". See, e.g.

twitter.com/brendaneich/status/585858406742786048

Also: Monadic for sure. Too late for promises. Sometimes you end up taking two steps.

# Raphael Mu (8 years ago)

In theory it's possible, but Promise.resolve automatically joins Promises for the sake of ergonomics.

# Brendan Eich (8 years ago)

And draft ES6 tried for monadic, but compatibility with Promises libraries (more than "convenience") prevailed.

# Mark S. Miller (8 years ago)

Since the non-monadic way won, I don't know that it is worth arguing about why. But it is ergonomic issues much deeper than convenience, and much more important than compatibility with existing libraries -- even if those two were adequate considerations by themselves.

The behavior of promises was inspired (via Joule channels) by the behavior of logic variables in concurrent logic programming languages (FCP, Parlog, GHC, Vulcan, Janus, ToonTalk). An unbound logic variable, like a variable in logic, is a placeholder representing some ground value. Which ground value is yet to be determined. An unbound logic variable is not itself a ground value. Equating two logic variables means that the same ground value, whatever that is, must be the solution to both of them. A concurrent process waiting for a logic variable to be solved (fulfilled, bound) is not triggered when it is merely equated to another logic variable.

The fact that the API is monad-like is an imperfect analogy I liked when it happened. But I now regret it because it has caused endless confusion. Likewise, I regret naming Ephemeron Tables "WeakMaps" (although anything is better than "Ephemeron Tables") because people tried to read into it a stronger analogy to weak references than was meant.

Having a promise become its value, as in E, would also only be possible if they were non-monadic, because it would make them more like concurrent logic programming logic variables.

To avoid a rehash of things we have already argued to exhaustion, I refer everyone to previous discussions. Please only respond here if you have something to say that has not already been said in those previous discussions, or if you'd like to provide links to those previous discussions.

# Tab Atkins Jr. (8 years ago)

On Sun, Feb 7, 2016 at 9:07 AM, Raphael Mu <encryptedredux at gmail.com> wrote:

The ES Promise is an instance of Monad, a property that implies a much more concise and expressive syntax for using Promise, by exploiting its monadic properties. I've seen a lot of people complain about Promises having too clumsy a syntax, and likewise for async/await.

As others have said, ES Promise is monad-like, but not actually a monad, because it flattens too much. That said, it's close enough for most purposes.

We now have the do-notation proposal (strawman:do_expressions), and monadic assignment would fit well into the syntax.

I assume you're reading this as being similar to do-notation in Haskell? The resemblance stops at the keyword - the do-notation in ES is just a way of converting a statement (or multiple) into an expression. Do-notation in Haskell is specifically and explicitly sugar for nested bind calls. Conflating them will likely just lead to sadness (Rick offers an example with a syntax conflict).

In any case, arrow functions actually make nested binds not too bad in JS:

let finalPromise = promiseA.then( x => promiseB.then( y => { let c = f(a,b); g(a, b, c); }));

Except for the fact that you need to track the parens/curlies you used, so you can close them at the end, this hews pretty close to Haskell's do-notation.

As an added benefit, you get support for functors/applicatives/tranversables/etc for free, by using .map/etc instead of .then (/.flatMap/.bind, for the general monad). Also, being able to customize the function used is nice for JS-as-it-exists, because we don't have a rich ecosystem of monadic/etc classes that use a common syntax yet: Promises use .then, Arrays have a proposal to add .flatMap, JS Fantasy Land uses .chain, etc.

The extension would allow use of <- within a do-expression for binding Promise values to variables, and the computation would behave most similarly to the Either monad in Haskell (in the following code, if promiseA or promiseB reject, the result of the entire expression would be a rejected Promise).

Even if we added explicit support for something like this, there'd be no reason to specialize it to Promises. It would just desugar to nested binds, same as Haskell.

# Isiah Meadows (8 years ago)

There is kind of a do-like syntax for Promises: async functions. To borrow Tab's example:

let finalPromise = (async () => {
  let x = await promiseA
  let y = await promiseB
  let c = f(a, b)
  return g(a, b, c)
})()

// or in parallel
let finalPromise = (async () => {
  let [x, y] = Promise.all([promiseA, promiseB])
  let c = f(a, b)
  return g(a, b, c)
})()
# /#!/JoePea (8 years ago)

On Tue, Feb 9, 2016 at 10:28 PM, Isiah Meadows <isiahmeadows at gmail.com> wrote:

let finalPromise = (async () => { let x = await promiseA let y = await promiseB let c = f(a, b) return g(a, b, c) })()

I think it's important to keep the async/await keywords because they give a more readable semantic description of what's happening. What about async do?

let finalPromise = async do {
    let a = await promiseA
    let b = await promiseB
    let c = f(a, b)
    g(a, b, c)
}

That reads better!

# Isiah Meadows (8 years ago)

I see little to be gained, and it's not clear that it's in a different context. Plus, assuming your editor balances parentheses and auto indents, I see no more than about 8-10 keystrokes saved for something that isn't super frequently used. Not saying it's a bad idea, but ES has gotten to the point most of the truly lacking features aren't in the language anymore, especially with ES7 and the first things on the train for ES8 (async functions barely missed it by a couple weeks, because of implementors).

# Alan Johnson (8 years ago)

If this is done, please go with async do { … await … }. May as well reuse existing syntax as much as possible. As mentioned, <- is problematic both from the standpoint of conflict with existing operators and incomplete analogy with Haskell. If the result should be Promise-ified, best to indicate this explicitly with async.

I do think that having a syntax for async expressions is very useful. IIFEs already feels like a hack around the lack of expression-orientedness of control flow structures in JS, and async-IIFEs just compound the issue of syntactic noise for straightforward operations.

# Brendan Eich (8 years ago)

We'll get it