Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

The real confusion here is dynamic typing. It's weird that the parameter to .then() can return any value, but if it's a promise there's special behavior. And for no good reason either

    p.then(r => r + 5);
Is just a bit of sugar for

    p.then(r => Promise.resolve(r + 5));
And then the type signatures are easy to reason about. The dynamic typing also risks introducing hard to find bugs. Suppose you have an heterogeneous array `things` which might contain numbers and promises which will resolve to numbers.

    p.then(r => things[r])
     .then(thing => selectedThings.push(thing));
You might intend `selectedThings` to also contain either numbers or promises that resolve to numbers, ordered deterministically as a function of `p`, but instead it will contain only numbers and its order is nondeterministic (it depends on `p` and all of the promise members of `things` that `p` ever signals).


This is exactly right. If the spec defined an API with simple semantics half these problems wouldn't exist. As it is `then` is overloaded to perform numerous distinct operations. This means it can't report any useful errors, because any value passed to `then` is valid. There was even discussion about this when the spec was being written, but it was decided the current semantics are more in keeping with Javascript. Unfortunately the spec authors were correct.


I don't agree with your first remark: what do you expect to be returned when you return a value not wrapped in a promise?

The ambiguity is actually the auto unwrapping of promises for subsequent then calls but I've yet to see a case where it's a problem (it's actually what makes the API clean looking when used).

For your last snippet, I don't see an array so I don't know what ordering you want to preserve (p always returns the same value so r will always be the same), if it's actually inside a loop, you should use Promise.all or Promise.map.


I think the confusion stems from the fact that .then() acts like either .map() or .flatMap() depending on the return type.

If .then() was just .flatMap(), you could expect an error if you didn't return a Promise (for example, reject with a TypeError).

If .then() was just .map(), you could return a value of any type (for example, string or Promise<number>) and get back a Promise for a value of that type (Promise<string> or Promise<Promise<number>>).


Actually, it's slightly more complex because if the result of the function passed to then were somehow a Promise<Promise<something>>, it would be flattened recursively so that the ultimate result is just Promise<something>. (Of course, due to .then's flattening semantics it's highly unlikely you'd have a Promise<Promise<...>> in the first places).

The distinction between map and flatMap makes sense for, say, lists, because you often do want to keep the non-flattened structure around. From promises, I can't think of a reason why that would be desired. You just want to represent the final result after all necessary waiting is completed. I suppose a library could provide stricter map and flatMap for people who deeply care about this.


what do you expect to be returned when you return a value not wrapped in a promise?

Nothing that's a type error, it should throw a runtime exception just like all type errors in javascript, and it will show up in the debugger for the programmer to fix.

The ambiguity is actually the auto unwrapping of promises for subsequent then calls but I've yet to see a case where it's a problem (it's actually what makes the API clean looking when used).

I'm not sure I quite understand what you're referring to, but I think you mean the fact that you can chain then calls

    Promise.resolve(5)
      .then(x => Promise.resolve(x + 5))
      .then(x => Promise.resolve(x + 5))
      .then(x => {
        // x == 15
      });
This is totally sound behavior. The simplified signature of `.then()` is

    then :: Promise a -> (a -> Promise b) -> Promise b
This makes `.then()` the `bind` function for the Promise monad. As a refresher for people who don't think about monads much:

    (>>=) :: Monad m => m a -> (a -> m b) -> m b
While on this tangent, I'll note that `Promise.resolve()` is the `return` function for the promise monad:

    return :: Monad m => a -> m a
    Promise.resolve :: a -> Promise a
For your last snippet, I don't see an array so I don't know what ordering you want to preserve

It's a contrived example demonstrating how switching based on whether the return value is a promise can make hard to detect bugs. It's hard to come up with a short example for this because strange values turning up where they don't belong is a property of large, dynamically typed systems. So yes, the example doesn't use all the best practices.


Well I can't really see any benefit to force the wrapping all returned values in promises without a proper example.

The then() signature can be expressed in typescript and I suppose in alternatives like flow (using the union type): https://github.com/borisyankov/DefinitelyTyped/blob/master/e... .


Sure, you can represent it that way, but it needlessly complicates the semantics of `then`. If they were merely monadic bind then it'd easily be 1/20th as confusing.

The "overload everything every which way and assume that people are really sensitive to the differences" strategy just begs misunderstanding.

Monadic bind as the core and basis of your API is fantastic. It is exactly enough and no more than you need to define effectful sequencing semantics and, if you like, you can build everything atop it. And, honestly, you do anyway. But right now the design obscures the simple basis technology in order to have heaps of overload magic.


He's stating the problem is dynamic typing... so the expectation would be a type error (!).


" (..) but if it's a promise there's special behavior. And for no good reason either."

Just because you don't know the reason doesn't mean there isn't one. This is especially the case with a community-driven spec like PromiseA+.


Yup, this is another example of people not really understanding promises in JS too well.

Having to do `Promise.resolve(r + 5)` doesn't really solve that much in terms of typing, namely the fact you can return a rejected promise or throw an error and so on.

As someone who wrote a promise library or two in typed languages - I don't see it as solving it at all, although what's usually done is that `Promise.resolve` is aliased to `ok` or something similar to simplify the boilerplate.


Promises are monads. `ok` is `return :: a -> Promise a` and `then` is `bind :: Promise a -> (a -> Promise b) -> Promise b`. All other behavior arises from that, the monad laws, and the operations

    throw :: Error -> Promise a
    reject :: Promise a      -- I guess, not sure of the semantics here




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: