Equivalent of Scala's for-comprehension using fp-ts
Tags: typescript, scala, functional, programming
Published: 2022-02-17 09:10:54

## Table of contents
* [Introduction](#introduction)
* [Prerequisites](#prerequisites)
* [Using fp-ts](#using-fpts)
* [Using fp-ts-contrib](#using-fptscontrib)
* [Summary](#summary)

---

**If you are only interested in the final result with some takeaways**, feel free to jump to the [Summary](#summary) section.

## Introduction

Hello and welcome!

If you come from the Scala world, and you are wondering how you can replicate a _for comprehension_ expression in TypeScript, then you may be interested in this article.

Today, we are going to see how we can write such an expression using TypeScript and the [fp-ts](https://github.com/gcanti/fp-ts) ecosystem.

The objective of this article is to transform the following Scala blocks of code:

### Case 1
```scala
def foo(): Option[String] = ???
def bar(a: String): Option[Int] = ???
def baz(b: Int): Option[Int] = ???

for {
  a <- foo()
  b <- bar(a)
  c <- baz(b) if b >= 42
} yield (b + c)

// Option[Int]
```

### Case 2
```scala
// Here, we are using some arbitrary `IO` data type.
// This could come from the cats-effect library for example.

def foo(): IO[String] = ???
def bar(): IO[Unit] = ???
def baz(a: String): IO[Int] = ???
def qux(b: Int): IO[Unit] = ???

for {
  a <- foo()
  _ <- bar()
  b <- baz(a)
  _ <- qux(b)
} yield (b + 1)

// IO[Int]
```

Both have some common behaviors: bindings to intermediate references such as `a` and `b`, and yielding a final value. In addition, the first one has a filtering step, and the second one has some discarded results (i.e. running side effects that do not return any value).

Depending on the version of fp-ts you are using, you might have to follow either the fp-ts or the fp-ts-contrib way. Please refer to the [Prerequisites](#prerequisites) section for more information.

In both cases, we are going to use the _do notation_.

## Prerequisites

* [fp-ts](https://github.com/gcanti/fp-ts) v2.8 or above
* Or, [fp-ts-contrib](https://github.com/gcanti/fp-ts-contrib) v0.0.2 or above

For both ways, here are the function declarations we are going to use:

### Case 1
```typescript
import * as O from 'fp-ts/Option'

declare function foo(): O.Option<string>
declare function bar(a: string): O.Option<number>
declare function baz(b: number): O.Option<number>
```

### Case 2
```typescript
import * as I from 'fp-ts/IO'

declare function foo(): I.IO<string>
declare function bar(): I.IO<void>
declare function baz(a: string): I.IO<number>
declare function qux(b: number): I.IO<void>
```

## Using fp-ts

* [Case 1](#using-fpts-case-1)
* [Case 2](#using-fpts-case-2)

### <a id="using-fpts-case-1"></a> Case 1

Let us build the _for comprehension_ expression step by step.

First, we have to start the _do notation_. There are 2 ways of doing this:
* Using `Do` and `bind`:
    ```typescript
    import * as O from 'fp-ts/Option'
    import { pipe } from 'fp-ts/function'
    
    pipe(
      O.Do,
      O.bind('a', () => foo())
    )
    ```

* Calling `foo`, then using `bindTo`:
    ```typescript
    import * as O from 'fp-ts/Option'
    import { pipe } from 'fp-ts/function'
    
    pipe(
      foo(),
      O.bindTo('a')
    )
    ```

There is no preferred way, although I personally like to make it explicit with the `Do` step.

> At this point, the type of this expression is `Option<{ a: string }>`.

---

**Tip**: on VS Code, you can inspect the type inferred by TypeScript with the help of an identity function and `ctrl + mouseover` the parameter (`cmd + mouseover` on Mac):

![Use of ctrl+mouseover on VS Code, over the x parameter of the identity function x => x, inserted as the last argument of the pipe(...) function](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a0wkfdd0yxwffqbsgnrz.png)

I use this identity function quite a lot to make sure I have made the correct value transformations, step by step.

---

The next step is `b <- bar(a)`. For this, we are going to use `bind`, while accessing the previous value _bound_ to the `a` reference:

```diff
pipe(
  ...,
+  O.bind('b', ({ a }) => bar(a))
)
```

In this step, the second argument of `bind` is a function whose parameter is a `{ a: string }`, i.e. whatever is inside the `Option` at this point. This allows us to read the previous value to call the `bar` function, and get a new value bound to the `b` reference.

> Following this step, the type has become `Option<{ a: string, b: number }>`.

The next line, `c <- baz(b) if b >= 42`, is the most complicated one. It is composed of 2 steps: a filter, and some binding of a value.

At the time of writing these lines (fp-ts v2.11.8), there is no function available that combines both these steps, so we have to write them explicitly:

```diff
pipe(
  ...,
+  O.filter(({ b }) => b >= 42),
+  O.bind('c', ({ b }) => baz(b))
)
```

---

We could create an intermediate function, `bindFilter`, that would combine these 2 steps:

```typescript
import { flow } from 'fp-ts/function'

const bindFilter: <N extends string, A, B>(
  name: Exclude<N, keyof A>,
  f: (a: A) => O.Option<B>,
  predicate: (a: A) => boolean
) => (ma: O.Option<A>) => O.Option<{ readonly [K in N | keyof A]: K extends keyof A ? A[K] : B }> =
  (name, f, predicate) => flow(O.filter(predicate), O.bind(name, f))

pipe(
  O.Do,
  ...,
  bindFilter('c', ({ b }) => baz(b), ({ b }) => b >= 42)
)
```

However, we would have to write a different version of `bindFilter` for each instance of Monad used in our code, e.g. for `Either`, `IO`, `Task`... This could be quite tedious just to abstract the `filter` + `bind` calls.

Alternatively, we could use `flow` to regroup these 2 steps into a single one:

```diff
+ import { flow } from 'fp-ts/function'

pipe(
  ...,
+  flow(O.filter(({ b }) => b >= 42), O.bind('c', ({ b }) => baz(b)))
)
```

---

> Following this new addition, the type of our expression has changed to `Option<{ a: string, b: number, c: number }>`.

Finally, we have the `yield (b + c)` part. This one can be achieved using `map`:

```diff
pipe(
  ...,
+  O.map(({ b, c }) => b + c)
)
```

> The final type has become `Option<number>`, which is what we were expecting to get.

Final result:
```typescript
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'

pipe(
  O.Do,
  O.bind('a', () => foo()),
  O.bind('b', ({ a }) => bar(a)),
  O.filter(({ b }) => b >= 42),
  O.bind('c', ({ b }) => baz(b)),
  O.map(({ b, c }) => b + c)
)
```

### <a id="using-fpts-case-2"></a> Case 2

A lot of what we are going to use has already been seen in the previous section, for the first case. The only difference here is that we have some steps that do not return any value.

First, let us start the _do notation_:

```typescript
import * as I from 'fp-ts/IO'
import { pipe } from 'fp-ts/function'

pipe(
  I.Do
)
```

Then, we can add the `a <- foo()` step:

```diff
pipe(
  I.Do,
+  I.bind('a', () => foo())
)
```

> So far, the type of our expression is `IO<{ a: string }>`.

Following this, we have our first case where we do not care about the returned value: `_ <- bar()`. Since we ignore the result of calling `bar`, we are going to use `chainFirst`:

```diff
pipe(
  ...,
+  I.chainFirst(() => bar())
)
```

> Here, we still have the same type for this expression (i.e. `IO<{ a: string }>`) since we ignored whatever `bar()` was returning.

Now comes the `b <- baz(a)` step. As seen in the previous section, we are going to use `bind`, while accessing the previous value _bound_ to the `a` reference:

```diff
pipe(
  ...,
+  I.bind('b', ({ a }) => baz(a))
)
```

> Following this step, the type has become `IO<{ a: string, b: number }>`.

The next step is another effect where we ignore the value it returns. However, now we need to access a previously bound value:

```diff
pipe(
  ...,
+  I.chainFirst(({ b }) => qux(b))
)
```

> The type of the expression is still `IO<{ a: string, b: number }>`, since we did not bind any new value.

Finally, we have the `yield (b + 1)` part, that we can handle using `map`:

```diff
pipe(
  ...,
+  I.map(({ b }) => b + 1)
)
```

> The final type has become `IO<number>`, which is what we were expecting to get.

Final result:
```typescript
import * as I from 'fp-ts/IO'
import { pipe } from 'fp-ts/function'

pipe(
  I.Do,
  I.bind('a', () => foo()),
  I.chainFirst(() => bar()),
  I.bind('b', ({ a }) => baz(a)),
  I.chainFirst(({ b }) => qux(b)),
  I.map(({ b }) => b + 1)
)
```

## Using fp-ts-contrib

* [Case 1](#using-fptscontrib-case-1)
* [Case 2](#using-fptscontrib-case-2)

### <a id="using-fptscontrib-case-1"></a> Case 1

Let us build the _for comprehension_ expression step by step.

First, we have to start the _do notation_:

```typescript
import { Do } from 'fp-ts-contrib/Do'
import * as O from 'fp-ts/Option'

Do(O.Monad) // or, Do(O.option) for fp-ts prior v2.11
```

Then comes the first step of this expression, `a <- foo()`. For this, we can use the `bind` method:

```diff
Do(O.Monad)
+  .bind('a', foo())
```

Next, we have `b <- bar(a)`, which requires us to use the `bindL` method to access the `a` reference:

```diff
Do(O.Monad)
  ...
+  .bindL('b', ({ a }) => bar(a))
```

Following this, we have the filtering part, `c <- baz(b) if b >= 42`. This is going to feel a bit "hacky" since, at the time of writing these lines (fp-ts-contrib v0.1.26), there is no `filter` method available in the `Do` builder.

To add this filter, we have to end the _do expression_ to get a `Option<{ a: string, b: number }>`, then apply the filter on it, then chain the filtered `Option` with a new _do expression_ to continue the computation:

```diff
+import { pipe } from 'fp-ts/function'
+
+pipe(
  Do(O.Monad)
    .bind('a', foo())
    .bindL('b', ({ a }) => bar(a))
+    .done(),
+  O.filter(({ b }) => b >= 42),
+  O.chain(({ a, b }) => Do(O.Monad)
+    .bind('c', baz(b))
+    .return(({ c }) => b + c)
+  )
+)
```

> In the fp-ts world, `chain` is the same thing as `flatMap`.

Here are some explanations to understand what is happening there:
* We end the first _do notation_ with `.done()`, which yields `Option<{ a: string, b: number }>`.
* We filter this value, using `O.filter` and a predicate.
* At this point, we still have a `Option<{ a: string, b: number }>`, but we need to go back in a `Do` builder. For that, the trick is to _chain_ the option we have with a new option that we will get with a new `Do` builder.
* By chaining, we have access to the previous references `a` and `b`. Be careful though, as they are not available in the new `Do` builder.
* We use the `.return(...)` method to yield an `Option<number>`, which is the type of value we are expecting.

Maybe someday we will have a `DoFilterable` builder that will feel more natural, as suggested in an [open issue](https://github.com/gcanti/fp-ts-contrib/issues/51) on the fp-ts-contrib repository.

Final result:
```typescript
import { Do } from 'fp-ts-contrib/Do'
import * as O from 'fp-ts/Option'
import { pipe } from 'fp-ts/function'

pipe(
  Do(O.Monad)
    .bind('a', foo())
    .bindL('b', ({ a }) => bar(a))
    .done(),
  O.filter(({ b }) => b >= 42),
  O.chain(({ a, b }) => Do(O.Monad)
    .bind('c', baz(b))
    .return(({ c }) => b + c)
  )
)
```

### <a id="using-fptscontrib-case-2"></a> Case 2

As usual, let us start the _do notation_:

```typescript
import { Do } from 'fp-ts-contrib/Do'
import * as I from 'fp-ts/IO'

Do(I.Monad) // or, Do(I.io) for fp-ts prior v2.11
```

The next step, `a <- foo()`, is pretty straightforward:

```diff
Do(I.Monad)
+  .bind('a', foo())
```

Following this, we have the first case where we ignore the returned value. For this, we can use the `do` method:

```diff
Do(I.Monad)
  ...
+  .do(bar())
```

Then, we can use `bindL` to implement the `b <- baz(a)` part:

```diff
Do(I.Monad)
  ...
+  .bindL('b', ({ a }) => baz(a))
```

After that, we have a second case where the returned value is ignored. However this time, we need a previous value: `_ <- qux(b)`. For that, we can use the `doL` method:

```diff
Do(I.Monad)
  ...
+  .doL(({ b }) => qux(b))
```

The last step, `yield (b + 1)`, can be achieved using the `return` method:

```diff
Do(I.Monad)
  ...
+  .return(({ b }) => b + 1)
```

Final result:
```typescript
import { Do } from 'fp-ts-contrib/Do'
import * as I from 'fp-ts/IO'

Do(I.Monad)
  .bind('a', foo())
  .do(bar())
  .bindL('b', ({ a }) => baz(a))
  .doL(({ b }) => qux(b))
  .return(({ b }) => b + 1)
```

## Summary

Sadly, there is no syntactic sugar available for monad composition in TypeScript. As fp-ts is one of the most popular functional libraries in this language, chances are you will use it to build functional programs.

Hopefully, this article can help you transpose your Scala knowledge regarding _for comprehension_ expressions into a TypeScript functional project, using the _do notation_.

Please, let me know if this is of any help :) 

Thank you for reading this far, and have a wonderful day!

### Using fp-ts

#### Case 1
```scala
for {
  a <- foo()
  b <- bar(a)
  c <- baz(b) if b >= 42
} yield (b + c)
```

```typescript
pipe(
  O.Do,
  O.bind('a', () => foo()),
  O.bind('b', ({ a }) => bar(a)),
  O.filter(({ b }) => b >= 42),
  O.bind('c', ({ b }) => baz(b)),
  O.map(({ b, c }) => b + c)
)
```

#### Case 2
```scala
for {
  a <- foo()
  _ <- bar()
  b <- baz(a)
  _ <- qux(b)
} yield (b + 1)
```

```typescript
pipe(
  I.Do,
  I.bind('a', () => foo()),
  I.chainFirst(() => bar()),
  I.bind('b', ({ a }) => baz(a)),
  I.chainFirst(({ b }) => qux(b)),
  I.map(({ b }) => b + 1)
)
```

#### Takeaways
* Start the _do notation_ with `Do` + `bind`
* To discard the result, e.g. `_ <- foo()`, use `chainFirst`
* To use a filter, e.g. `x <- foo() if y`, use `filter` + `bind`
* End the _do notation_ (`yield x`) with `map`

### Using fp-ts-contrib

#### Case 1
```scala
for {
  a <- foo()
  b <- bar(a)
  c <- baz(b) if b >= 42
} yield (b + c)
```

```typescript
pipe(
  Do(O.Monad)
    .bind('a', foo())
    .bindL('b', ({ a }) => bar(a))
    .done(),
  O.filter(({ b }) => b >= 42),
  O.chain(({ a, b }) => Do(O.Monad)
    .bind('c', baz(b))
    .return(({ c }) => b + c)
  )
)
```

#### Case 2
```scala
for {
  a <- foo()
  _ <- bar()
  b <- baz(a)
  _ <- qux(b)
} yield (b + 1)
```

```typescript
Do(I.Monad)
  .bind('a', foo())
  .do(bar())
  .bindL('b', ({ a }) => baz(a))
  .doL(({ b }) => qux(b))
  .return(({ b }) => b + 1)
```

#### Takeaways
* Start the _do notation_ with `Do(monadInstance)`
* To discard the result, e.g. `_ <- foo()`, use `doL` if you require a previous value, or `do` if you do not
* End the _do notation_ (`yield x`) with `return` if you need to compute some value, or `done` if you want to preserve the "context" object containing all the references
* To use a filter, e.g. `x <- foo() if y`, end the _do notation_ with `done`, then use `filter`, then chain with a new _do notation_, with `chain` and `Do(monadInstance)`

More information regarding this _do notation_ on the following article: [Do syntax in TypeScript](https://paulgray.net/do-syntax-in-typescript/).