Let's say a promise is a thing which can either succeed or fail eventually. If it fails, it gives a type e, if it succeeds a type a
Promise e a
Now, `then` operates on the successful result, transforming it into a new promise of a different kind. The result is a total promise of the new kind then :: Promise e a -> (a -> Promise e b) -> Promise e b
I'll contest now that this is sufficient. Here's what you appear to lose: 1. No differentiation of error types
2. No explicit annotation of the ability to
return constant/non-promise values
3. No tied-in error handling
That's fine, though. First, for (1), we'll note that it ought to be easy to provide an error-mapping function. This is just a continuation which gets applied to errors upon generation (if they occur) mapError :: (e -> e') -> Promise e a -> Promise e' a
For (2) we'll note that it's always possible to turn a non-promised value into a promise by returning it immediately pure :: a -> Promise e a
Then for (3), we can build in error catching continuations catch :: Promise e a -> (e -> Promise e' a) -> Promise e a
We appear to lose the ability to change the result type of the promise upon catching an error, but we can regain that by pre-composition with `then`.So, each of these smaller types is now very nice to work with. They are equivalent in power to the fully-loaded `then` you gave, but their use is much more compartmentalized. This is how you avoid frightful types.