Published on

# Functional Programming: Mappables & Chainables

Authors
• Name
Zafer Cesur
@zafer-cesur
Occupation
Co-founder & CTO

This post is a brief foray into the world of functional programming. For demonstration purposes, we'll be using TypeScript since it is perhaps the most approachable and mainstream language that could pass as a statically typed FP language. Some examples will additionally be relying on the `fp-ts` library.

Now, unlike most posts on FP that you might see on the internet, this one will neither be theoretical nor thorough. If you're interested in something like that, I'd recommend checking out the excellent book Professor Frisby’s Mostly Adequate Guide to Functional Programming.

Instead, we'll be handwavy at times and take a more practical approach by starting with problems and working towards solutions which employ concepts that make FP so great such as data transformation pipelines, higher-order functions and composability.

Enough rambling—let's get to it! First we should define some cats

``````type Cat = {
name: string
age: number
food: Array<string>
}

const maple = { name: 'maple', age: 1, food: ['salmon', 'mackerel'] }
const lyon = { name: 'lyon', age: 2, food: ['chicken', 'eggs'] }
const beans = { name: 'beans', age: 3, food: ['pinto beans'] }

const cats = [maple, lyon, beans]
``````

and a simple function that we can use on our cats:

``````const age: (c: Cat) => number = (c) => c.age

age(maple)
// 1
``````

# Higher-order functions

Here's a problem: how can we use this function to get the ages of all of our cats? Easy peasy, you might say. Just `map` it on the array like so:

``````cats.map(age)
// [1, 2, 3]
``````

Very well! Here is another challenge: suppose we also wanted to get an array consisting of the favorite food of our cats. Shall we use `map` again? Let's see what happens.

``````const food: (c: Cat) => Array<string> = (c) => c.food

food(maple)
// ["salmon", "mackerel"]

cats.map(food)
// [["salmon","mackerel"],["chicken","eggs"],["pinto beans"]]
``````

Hmm, we get an array of arrays—not quite what we wanted. Luckily there is another function just for these kinds of situations: `flatMap`!

``````cats.flatMap(food)
// ["salmon", "mackerel", "chicken", "eggs", "pinto beans"]
``````

Problem solved.

# Currying

Are you feline spicy?

While the above solution's all well and good, let's try to solve these problems in a slightly different way—for reasons that will become clear later in this post. In order to do that, we'll need to shift our perspective a little.

So both `map` and `flatMap` take an array and a function, and return another array. What if they only took a function and returned another function that takes an array? In other words, what if they "lifted" our regular functions and turned them into array functions? There are analogues of `map` and `flatMap` in the `fp-ts` library that work just like that, called `map` and `chain`, respectively. Let's see them in action:

``````import * as A from 'fp-ts/lib/Array'

const age: (c: Cat) => number = (c) => c.age
const ages: (a: Array<Cat>) => Array<number> = A.map(age)

age(maple)
// 1

ages(cats)
// [1, 2, 3]

const food: (c: Cat) => Array<string> = (c) => c.food
const foods: (a: Array<Cat>) => Array<string> = A.chain(food)

foods(cats)
// ["salmon", "mackerel", "chicken", "eggs", "pinto beans"]
``````

So, in a nutshell,

• `map` turns functions from `A` to `B` into those from `Array<A>` to `Array<B>`
• `chain` turns functions from `A` to `Array<B>` into those from `Array<A>` to `Array<B>`

Okay, that's kind of cool... I guess. But why do we care, right? As we've already seen, we can just use `map` and `flatMap` to solve the exact same problems. Plus, we wouldn't need to import an entire library.

# Error handling

In order to really appreciate `map` and `chain`, we need to look at some other data types that are not arrays. To motivate our next data type, consider this: how might we write a function that returns a cat given a name? Since we don't have a cat ready for all possible names, we need to handle the case where we are asked a cat we don't have.

## Exception

How about we just throw an error?

``````//                                 This is a lie
//                                       v
const catFromNameExn: (name: string) => Cat = (name) => {
switch (name) {
case 'maple':
return maple
case 'lyon':
return lyon
case 'beans':
return beans
default:
}
}
``````

The good bad news is that this piece of code compiles fine. But we're blatantly lying here when we say in the signature that our function always returns a `Cat` when most of the time it actually throws an error. In order to use this function properly one needs to read the documentation at best or the actual code at worst. Can we do better? Can we perhaps lift this insight that we have about missing cats into the type system?

## Union type

``````//                              Proudly admitting the truth
//                                           v
const catFromName: (name: string) => Cat | Error = (name) => {
switch (name) {
case 'maple':
return maple
case 'lyon':
return lyon
case 'beans':
return beans
default:
}
}
``````

This changes the return type of our function into a union of `Cat` and `Error` types. That means, if someone uses this function and doesn't handle the error case, they will get a compile-time error:

``````const cat = catFromName('max')
cat.age
// Property 'age' does not exist on type 'Cat | Error'.
//   Property 'age' does not exist on type 'Error'.ts(2339)
``````

Nice, we have literally made it impossible to misuse our function! How about another function that returns the favorite type of beans of a cat?

``````const favBeans: (c: Cat) => string | Error = (c) => {
for (const f of c.food) {
if (f.includes('beans')) {
return f
}
}
return Error('Provided cat does not eat beans')
}
``````

Cool beans! Now we should be able to combine these into a single function that returns the favorite type of beans of a cat, given only its name:

``````const favBeansFromName: (name: string) => string | Error = (name) => {
const catOrErr = catFromName(name)
if (catOrErr instanceof Error) {
return catOrErr
}

const beansOrErr = favBeans(catOrErr)
if (beansOrErr instanceof Error) {
return beansOrErr
}

return beansOrErr
}

favBeansFromName('max')

favBeansFromName('beans')
// "pinto beans"
``````

Hmm... This certainly works fine, but 2/3s of the code here is just to handle the errors. For such a simple function, this is embarrassingly hard to read because of all the noise. Compare it with the exception-throwing version:

``````const favBeansFromNameExn: (name: string) => string = (name) => {
const cat = catFromNameExn(name)
const beans = favBeansExn(cat)
return beans
}
``````

So are we doomed to choose between safety and ergonomics? Luckily, the answer is no. Enter `Either`.

## Either

``````// fp-ts/lib/Either
type Either<E, A> = Left<E> | Right<A>
``````

`Either` builds upon our idea of using a union type. The difference is that, we construct an instance of `Either` by wrapping our value in `Left` or `Right` instead of using it directly. By convention, `Left` is used to hold an error value and `Right` is used to hold a correct value. Let's see it in action:

``````import * as E from 'fp-ts/lib/Either'
import Either = E.Either

const catFromName: (name: string) => Either<Error, Cat> = (name) => {
switch (name) {
case 'maple':
return E.right(maple)
case 'lyon':
return E.right(lyon)
case 'beans':
return E.right(beans)
default:
}
}
``````

So our new function neither returns a cat nor an error, but instead it returns a box that contains either a cat or an error. Kinda like the Schrödinger's box! But what do we do with this box? For instance, how can we take the cat outside the box, assuming there is one, so that we can find out how old she is?

What if we didn't have to do all that? Remember how we didn't actually have to concern ourselves with the internals of our `cats` array earlier when we applied the `age` function to each cat? Drawing a parallel to arrays, the `Either` type can essentially be thought of as a container: if it's a "left" box, it has 0 (meaningful) elements, and if it's a "right" box, it has precisely 1 element.

Using the same idea, we can lift our function that operates on cats into a function that operates on Schrödinger's boxes, a.k.a. `Either`s. To do that, you don't need to look any further than our trusted `map`—this time the `Either` instance of it:

``````// If you squint a little, you might notice that Either<Error, _> is just
// like Array<_>
const age: (c: Cat) => number = (c) => c.age
const ageA: (a: Array<Cat>) => Array<number> = A.map(age)
const ageE: (e: Either<Error, Cat>) => Either<Error, number> = E.map(age)

const mapleBox = catFromName('maple')
const maxBox = catFromName('max')

ageE(mapleBox)
// E.right(1)

ageE(maxBox)
``````

How does this work? Well, our lifted function `ageE` takes a box and checks what kind of box it is behind the scenes. If it's a right box, it applies `age` to the cat inside; otherwise it does nothing and returns the box as is. By the same token, we can write

``````const favBeans: (c: Cat) => Either<Error, string> = (c) => {
for (const f of c.food) {
if (f.includes('beans')) {
return E.right(f)
}
}
return E.left(Error('Provided cat does not eat beans'))
}

const favBeansE: (e: Either<Error, Cat>) => Either<Error, string> = E.chain(favBeans)

favBeansE(mapleBox)
// E.left(Error("Provided cat does not eat beans"))

favBeansE(catFromName('beans'))
// E.right("pinto beans")
``````

So once we write a cat function, we can reuse it on all kinds of different data types (like `Array`, `Either` etc.) by just lifting it into the appropriate context using `map` or `chain`!

What's more is that by utilizing a helper function called `pipe` we can do away with defining these intermediate functions like `ageE` or `favBeansE` by composing these smol cat functions into a pipeline:

``````import { pipe } from 'fp-ts/lib/function'

pipe(
catFromName('maple'),
E.chain(favBeans),
E.map((s) => s.toUpperCase())
)
// E.left(Error("Provided cat does not eat beans"))
``````

If you'd like a bit more symmetry, we could equivalently also put the name inside a right box and chain `catFromName` like so:

``````pipe(
E.of('beans'), // same as E.right("beans")
E.chain(catFromName),
E.chain(favBeans),
E.map((s) => s.toUpperCase())
)
// E.right("PINTO BEANS")
``````

Finally we are fully equipped to rewrite our function above in a fully type-safe and ergonomic way:

``````const favBeansFromName: (name: string) => Either<Error, string> = (name) =>
pipe(E.of(name), E.chain(catFromName), E.chain(favBeans))
``````

Aaand that's it. Notice how there isn't a single line of error handling code! We get all of that for free from the implementations of `map` and `chain` for the `Either` data type, which short-circuits remaining computations upon the first error. It works just as before:

``````favBeansFromName('max')

favBeansFromName('maple')
// E.left(Error("Provided cat does not eat beans"))

favBeansFromName('beans')
// E.right("pinto beans")
``````

# Closing notes

`Array` and `Either` together form just the tip of the iceberg that is the mappable and chainable data types, which are technically known as functors and monads, respectively. These data types appear everywhere and are used to model a wide range of kinds of computation. To give a few examples from the `fp-ts` library, along with what each of them represents,

Moreover, we can stack these mappable/chainable structures on top of one another to get new mappable/chainable structures that have all the encapsulated features. For example, we have

and so on, all with their own implementations of `map` and `chain` derived from those of each individual structure.

## Orthogonal design leads to simplicity

As a final closing note, let's consider how our `favBeansFromName` function would look like if the functions that it's composed of (e.g. `favBeans` and `catFromName`) were of a different kind. What do I mean? In our example, these functions were such that they could fail. As a reminder, this is how it looked like:

``````import * as E from 'fp-ts/lib/Either'
import Either = E.Either

const favBeansFromName: (name: string) => Either<Error, string> = (name) =>
pipe(E.of(name), E.chain(catFromName), E.chain(favBeans))
``````

What if these functions were such that they returned arrays instead? For example they could return a singleton array if the function is successful and an empty one otherwise. Then our pipeline would look like this:

``````import * as A from 'fp-ts/lib/Array'

const favBeansFromName: (name: string) => Array<string> = (name) =>
pipe(
A.of(name), // same as [name]
A.chain(catFromName),
A.chain(favBeans)
)
``````

What if they were asynchronous functions that always resolved successfully? Maybe calling `catFromName` would make a call to a server which would then create a cat with that name in the database and return that, or something. You get the idea. Then, we'd have

``````import * as T from 'fp-ts/lib/Task'

const favBeansFromName: (name: string) => Task<string> = (name) =>
pipe(
T.of(name), // same as () => Promise.resolve(name)
T.chain(catFromName),
T.chain(favBeans)
)
``````

Let's do a final one. What if we had the original failable functions, but they were now asynchronous? Then, our code would look something like

``````import * as TE from 'fp-ts/lib/TaskEither'

const favBeansFromName: (name: string) => TaskEither<Error, string> = (name) =>
pipe(
TE.of(name), // same as () => Promise.resolve(E.of(name))
TE.chain(catFromName),
TE.chain(favBeans)
)
``````

Isn't this great? No matter what kind of functions we are working with, our code looks exactly the same. There's no error-handling in the `Either` case, no looping in the `Array` case et cetera. Even the asynchronous code looks the same as the ordinary synchronous one.

Now, consider the `Promise` type along with the toolchain that comes with it:

• `then` is kinda like both `map` (if you return a regular value) and `chain` (if you return a `Promise`), because it is overloaded to encompass both functionalities.
• `catch` is used for error handling semantics, and
• `await` syntax makes it possible to structure async code in a way that is similar to synchronous code.

Nevertheless, all of these additional things add to the complexity of the language. In contrast, by adopting the FP toolchain, we can

• use the same `map` and `chain` syntax for all kinds of computations, each with its own semantics—which makes overloaded methods like `then` redundant
• separate concerns orthogonally into different mappable/chainable data types (e.g. `Task` for async and `Either` for error handling) that we can compose (e.g. `TaskEither`)—which makes methods and control flow structures such as (`try`)-`catch` redundant
• employ data pipelines and higher-order functions to structure code that looks the same whether it is synchronous, asynchronous, error-handling etc.—which makes language features like `await` redundant

It goes to show that, instead of adding ad-hoc syntax and semantics, we can achieve the same, if not more of the desired features by choosing the right abstractions.