4

I know that Javascript's promises are technically neither functors nor monads in the sense of Haskell, because (among other things)

  • they include a bind operation that falls back to map when a pure function is passed in (and thus has an ambiguous type)
  • both the Promise constructor and resolve (aka return) join nested promises recursively

The first issue can easily be bypassed by always providing a function with the right type a -> Promise b.

The second issue obviously violates the parametricity trait of parametric polymorphic functions, i.e. one cannot construct a m (m a) structure. But what would this structure mean in the context of promises/asynchronous computations? I cannot think of a meaningful semantics for Promise (Promise a), where Promise is a monad. So what do we lose? What are the implications of the recursive joining?

Provided we are pretty pragmatic (and that's what we should be when we're programming Javascript), can't we claim that a Promise is a monad in Javascript if we take care of the edge cases?

  • 3
    I would consider that to be a monad. Being used to Haskell, I find the automagic processing (join, map) a bit confusing, but underneath that magic there's still a monad. I guess if I really wanted a less magic behavior I could wrap the promise inside some object, and construct something like `m (Identity (m a))`, so that the join does not happen. Inconvenient, but doable. – chi Aug 19 '17 at 11:34
  • 4
    Well, do the laws hold? That's all you have to check. – Zeta Aug 19 '17 at 11:57
  • 2
    As you've noted, for promises to have any chance of being a monad, you'll have to do some handwaving. So begin with some hypothetical operations similar to the ones implemented by promises (i.e. `return` which doesn't also `join`, `bind` which has an unambiguous type) and then prove the monad laws for these functions. Once you've done that, attempt to concretely implement these in terms of actual promise functions. In my mind, this would be a reasonable proof - the 1st (and more important) part being formal, the 2nd being 'informal' or hand-wavy. – user2407038 Aug 19 '17 at 12:07
  • 2
    "*I cannot think of a meaningful semantics for `Promise (Promise a)`*" - they're the same semantics as for any other `Promise x`, and that's what makes them meaningful. – Bergi Aug 19 '17 at 12:46
  • @Zeta Indeed, and in this case the laws don't hold: https://stackoverflow.com/a/50173415/1614973 – Dmitri Zaitsev May 29 '19 at 03:10

2 Answers2

4

The first issue can easily be bypassed by always providing a function with the right type a -> Promise a.

Or by not using then as the bind operation of the monad, but some type-correct ones. Creed is a functionally minded promise library that provides map and chain methods which implements the Fantasy-land spec for algebraic types.

The second issue can be bypassed as well with the same approach, by not using resolve but fulfill instead, and the static of method as the unit function.

But what would this structure mean in the context of promises/asynchronous computations?

It's a promise for a promise for a value. Not every constructible type needs to be "meaningful" or "useful" :-)

However, a good example of a similar type is provided by the Fetch API: it returns a promise that resolves to a Response object, which again "contains" a promise that resolves to the body of the response.

So a Promise (Promise a) might have only one success result value, which could as well be accessed through a Promise a, however the two levels of promises

  • might fulfill at different times, adding a "middle step"
  • might reject with different causes - e.g. the outer one representing a network problem while the inner one represents a parsing problem

Notice that the Promise type should have a second type variable for the rejection reason, similar to an Either. A two-level Promise err1 (Promise err2 a) is quite different from a Promise err a.

I know that Javascript's promises are technically neither functors nor monads in the sense of Haskell

You haven't mentioned the biggest issue yet, however: they're mutable. The transition from pending to settled state is a side effect that destroys referential transparency if we consider execution order, and of course our usual use cases for promises involve lots of IO that isn't modelled by the promise type at all.

Promise.delay(50).then(() => Promise.delay(50))
// does something different than
const a = Promise.delay(50); a.then(() => a)

Applying the monad laws is fun and occasionally useful, but we need lots of pragmatism indeed.

Enlico
  • 23,259
  • 6
  • 48
  • 102
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • "_destroys referential transparency if we consider execution order_". This is very interesting. I need to reason about it... –  Aug 19 '17 at 14:58
  • Also when promises are nested the inner ones would be able to access the outer promises scope. This might be useful to pass values directly without using global variables or returning objects by means of object destructuring to pass multiple arguments to the next `.then` stage. – Redu Aug 19 '17 at 16:09
  • @Redu Not sure, do you mean that in `ppx.then(px => px.then(x => …` the `px` promise is scope? I don't see how that's particularly useful. We [can always "pass" values by simply nesting `then` calls](https://stackoverflow.com/a/28250687/1048572), similar to what `async`/`await` or monadic `do` notation desugar, we don't need promises of promises for that. – Bergi Aug 19 '17 at 18:05
  • I dont think in JS by any means you can do like `ppx.then(px => px.then(x => ...)` at least not in the sense of `Promise err1 (Promise err2 a)`. What i'd meant was `Promise.resolve(2).then(v => (y = (x => 2*x)(v), Promise.resolve(y+3))).then(v => console.log(v));`. Where the nested promise depends on the resolution of the previous. In Haskell though my promise data type would be like `data Promise a b = Resolve a | Reject b` for instance where `a` in fact could be a data type including another promise. – Redu Aug 19 '17 at 20:43
  • @Redu Yes, you cannot do `Promise err1 (Promise err2 a)` with native promises, but that's exactly what we're talking about here. The thing you did with the (global) `y` variable has nothing to do with promises, you can do that in any function. – Bergi Aug 19 '17 at 22:09
  • @Bergi Well sorry... obviously it was meant to be `var y`. By the way i think Haskell promise type would look more JS ish if defined like `data Promise a b = Resolve a b | Reject b` or would it be more meaningful if done like `data Promise a b = Then a b | Catch b`..? – Redu Aug 19 '17 at 22:35
  • @Redu It's more like `newtype Promise a b = Promise (Mvar (Either a b))` I think – Bergi Aug 20 '17 at 09:22
  • "Applying the monad laws is fun and occasionally useful, but we need lots of pragmatism indeed." --- There is a lot of pragmatism in using monads for writing safe and composable code. And less pragmatism in spending time on debugging and fixing subtle errors due to leaky abstractions. – Dmitri Zaitsev May 29 '19 at 03:28
  • @DmitriZaitsev Leaky abstractions still allow writing composable code, that's why I said they're useful. If you want to reason about safety and concurrency, then promises are probably the wrong monad type for it. – Bergi May 29 '19 at 09:18
  • They are certainly useful but having proper monad/functor with chain/map methods would likely have been more useful and safer. Promises are basically written for people too lazy to distinguish between `map` and `chain`, but there is price to pay in which it is harder to see the code intention and catche subtle errors due to the magic. – Dmitri Zaitsev May 29 '19 at 12:56
  • @DmitriZaitsev Oh, you were referring to that. Yes, I'd prefer `chain`+`map` as well, but even an implementation like Creed that heeds the type signatures cannot provide true monadic promises - the abstraction is leaky as explained in the last paragraph of my answer. – Bergi May 29 '19 at 15:05
  • I wonder what you think of https://github.com/dmitriz/cpsfy as replacement for promises? It is meant to provide all promises' benefits along with correct functional interfaces (and going beyond functors and monads towards their multi-args versions.) – Dmitri Zaitsev May 30 '19 at 04:16
1

I know that Javascript's promises are technically neither functors nor monads in the sense of Haskell

Not only in the sense of Haskell, in any other way as well.

  • they include a bind operation that falls back to map when a pure function is passed in (and thus has an ambiguous type)

there is no bind operator provided by JS native promises

  • both the Promise constructor and resolve (aka return) join nested promises recursively

I presume you mean unwrapping "theneables", i.e. calling functions stored under then prop whenever there is such function.

The first issue can easily be bypassed by always providing a function with the right type a -> Promise b.

This would not resemble map e.g. when map(f) is used for f = x => {then: a => a}.

The second issue obviously violates the parametricity trait of parametric polymorphic functions, i.e. one cannot construct a m (m a) structure.

Indeed.

But what would this structure mean in the context of promises/asynchronous computations? I cannot think of a meaningful semantics for Promise (Promise a), where Promise is a monad. So what do we lose? What are the implications of the recursive joining?

You need to allow storing arbitrary values. Promises are not allowed to store theneables (without unwrapping), which is the problem. So you need to change semantics of both objects and methods. Allow objects to store theneables without change and implement .bind aka .chain that unwraps (or joins) theneables precisely once - no recursion.

This is what creed does for promise-like objects and cpsfy for callback-based (aka continuation passing style) functions.


Provided we are pretty pragmatic (and that's what we should be when we're programming Javascript), can't we claim that a Promise is a monad in Javascript if we take care of the edge cases?

Writing safe, succinct and composable code is pragmatic. Risking introduce subtle bugs via leaky abstractions that might crash critical software with far going consequences is not. Every edge case is a potential source of such risk.

In that respect, claiming that Promise is a monad does more harm than help, beside being incorrect. It does harm because you cannot safely apply monadic transformations to promises. E.g. it is unsafe to use any code conforming to the monadic interface with promises as if they were monads. If used correctly, monads are there to help abstracting and reusing your code, not to introduce lines of checking and hunting for edge cases.

Dmitri Zaitsev
  • 13,548
  • 11
  • 76
  • 110