Emulating the "do" notation for Maybe type (with functions).
See original GitHub issueI came up with an idea to implement a loose equivalent of haskell’s do notation with functions.
Introduction
A “do” notation allows for much cleaner code, with all the values “unwrapped” from their monadic context. Under the hood, it’s always a sequence of flatMap
(also known as bind
, chain
or >>=
) calls finished with a map
call.
In JavaScript/TypeScript, an async/await
is based on the same concept (there’s even an excellent article about that - async/await is just the do-notation of the Promise monad. However, it is restricted to promises (or thenables) only. Scala calls it “for comprehensions”.
Examples
In an ideal world, it could look like this (similar to haskell/scala):
const maybeAnAnswer = do {
const t <- Maybe.Just(5);
const u <- Maybe.Just(t + 11);
yield (t + u) * 2;
}
expect(maybeAnAnswer).toEqual(Maybe.just(42));
Notice how t
and u
(unwrapped with <-
operator) are available in every following line - they are “in scope” of the entire “do” block. Also, the last line has access to these values. Although it seems to be yielding a numeric value, it actually resolves to Maybe<number>
- in that case the value returned is Just(42)
.
Now let’s look at what happens if Nothing
creeps in:
const maybeAnAnswer = do {
const t <- Maybe.Just(5);
const u <- Maybe.nothing(); // whoopsie!
yield (t + u) * 2;
}
expect(maybeAnAnswer).toEqual(Maybe.nothing());
In this example we have a Nothing
in the middle of our computations. But we’re still safe! The last line (yield ...
) was actually not executed at all and we ended up with a safe Maybe
value, this time receiving Nothing
.
Idea
Because this syntax will probably never make it to ECMAScript specification (or will it?), I decided to try to implement a similar feature using functions. Here’s the first “do” block translated into a function:
const maybeAnAnswer = Maybe.do2(
Maybe.just(5),
(t: number) => Maybe.of(t + 11),
(t: number, u: number) => (t + u) * 2
);
expect(maybeAnAnswer).toEqual(Maybe.just(42));
The value unwrapped from the first Maybe
is available in every subsequent lambda. This means that the value of t
parameter will be 5
in both following functions. The value unwrapped from the first lambda (t + 11
) will become available under u
parameter of the last function, which is an equivalent of the yield
expression from previous examples.
Similarly:
const maybeAnAnswer = Maybe.do2(
Maybe.just(5),
(t: number) => Maybe.nothing(), // whoopsie!
(t: number, u: number) => (t + u) * 2
);
expect(maybeAnAnswer).toEqual(Maybe.nothing());
Because one of the values to unwrap was Nothing
, the result of the whole function call is Nothing
as well.
That’s cool, but why did you call it .do2
?
Well, that’s simply because of the limitations of the language.
In both haskell and scala one can use as many unwrapping expressions as she wants - these languages’ compilers will figure out what’s going on (that’s probably also the case for JavaScript’s async/await
- nothing stops you from await
ing for as many promises as you could only imagine in a single block of code).
In this case, I have to explicitly say how many Maybe
monads I’d like to unwrap - so do2
stands for two unwrapping “steps” (resulting in t
and u
values). I plan to add do3
, do4
and do5
, too (I think that five unwrapping steps should be more than enough for 99% of cases). Oh, and did I mention that it’s also type-safe? 😉
When?
I’m working on it. My quick proof of concept proved to be working (I have already created do2
and do3
and I even have some unit tests for them), but it’s going to take time to write some documentation.
Please, let me know what do you think about this idea!
Issue Analytics
- State:
- Created 5 years ago
- Comments:10
Top GitHub Comments
If anyone is interested, Ramda has taken a different approach. You can see what we discussed in https://github.com/ramda/ramda/pull/2512 and implemented in https://github.com/ramda/ramda/pull/2515 and https://github.com/ramda/ramda/pull/2630. We build a generic version of what had been
composeK
(Kleisli) andcomposeP
(Promises) that takes the unwrapping function:And the issue that started us off, piping possibly
nil
-valued functions through a pipeline (withoutMaybe
s) can now be handled withThere’s also
pipeWith
, a version ofpipe
for doing acomposeWith
with the functions reversed.No worries!