question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Emulating the "do" notation for Maybe type (with functions).

See original GitHub issue

I 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 awaiting 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:closed
  • Created 5 years ago
  • Comments:10

github_iconTop GitHub Comments

2reactions
CrossEyecommented, Dec 17, 2018

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) and composeP (Promises) that takes the unwrapping function:

composeWith(chain, [fn3, fn2, fn1]) // (for Monad-returning functions)
composeWith(then, [fn4, fn3, fn2, fn1]) // (for Promise-returning functions)

And the issue that started us off, piping possibly nil-valued functions through a pipeline (without Maybes) can now be handled with

const nonNil = (fn, val) => isNil(val) ? val : fn(val)
composeWith(nonNil, [fn3, fn2, fn1]))

There’s also pipeWith, a version of pipe for doing a composeWith with the functions reversed.

1reaction
chriskrychocommented, Oct 25, 2018

No worries!

Read more comments on GitHub >

github_iconTop Results From Across the Web

Simulating Haskell's do notation in Typescript - Medium
Haskell has convenient syntax for monads called “do notation” that ... A simple type built into Haskell is the Maybe type (which is...
Read more >
Haskell/do notation - Wikibooks, open books for an open world
Returning values​​ The last statement in do notation is the overall result of the do block. In the previous example, the result was...
Read more >
Type application with do-notation (do @Maybe ...) : r/haskell
Adding type application to do is fundamentally different syntactically. Unlike most things in Haskell, do is not a function. It's built-in ...
Read more >
Should do-notation be avoided in Haskell? - Stack Overflow
It is clear that the do notation OR the functions (>>=) and (>>) are needed for sequencing IO actions, and that the do...
Read more >
4. Monads — Programming in Lean 3.4.2 documentation
Monads¶. In this chapter, we will describe a powerful abstraction known as a monad. A monad is a type constructor m : Type...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found