This is a Typescript translation of the classic Functors, Applicatives, And Monads In Pictures.

Functor/Applicative/some of Monad is functional programming is the concept of “foundation”, anyway, I don’t agree with ‘base’ this argument, the author also read many articles similar to introduce Monad, finally go away, these concepts are more difficult to understand, And it’s hard to get to those things in programming practice.

Functors, Applicatives, And Monads In Pictures. So I want to digest this article a bit more in translation, using Typescript as the description language is easier for the front end to understand.

Please correct any misunderstandings. Let’s go!


This is a simple value:

Such as these

1        // number
'string' // string
Copy the code

Everyone knows how to apply a function to this value:

// So easy
const add3 = (val: number) = > val + 3
console.log(add3(2)) / / 5
Copy the code

Easy. Let’s extend it so that any value can be wrapped in a ** context **. Now you can imagine a box that you can put values in:

Now when you apply a function to the wrapper value, you get different results depending on the context type. This is the basis of Functor, Applicative, Monad, Arrow and the like.

Maybe is a typical datatype that defines two related contexts. Maybe itself is a context (can any type be a context except a value?). :

The article is based on Haskell, whose Maybe type has two contexts Just(blue box) and None(red empty box). Copying Haskell In Typescript we can use optional types like Maybe:

type Maybe<T> = Just<T> | Nothing // Just indicates that a value exists, and Nothing indicates that a value is null
Copy the code

The basic structure of Just and Nothing:

// We will replace null with None, where None is a value, not a class
export class None {}
// Corresponds to the type of None
export type Nothing = typeof None

// Check if it is Nothing, using Typescript 'Type Guards'
export const isNothing = (val: any): val is Nothing => {
  return val === None
}

/ / Just type
export class Just<T> {
  static of<T>(val: T) {
    return new Just(val)
  }
  value: T
  constructor(value: T) {
    this.value = value
  }
}
Copy the code

Example:

let a: Maybe<number>;
a = None;
a = Just.of(3);
Copy the code

To be honest, this implementation is a bit clunky, but I’ll use it for the moment to get closer to the original description. One version that was considered was the following, which was abandoned because it was impossible to extend methods to them:

  type Optional<T> = NonNullable<T> | nul
  let a: Optional<number> = 1;
  a = null;
Copy the code


We will soon see the difference between a function application of Just and a function of Nothing. First let’s look at Functor!





Functors

When a value is wrapped in a context, you can’t apply it to a normal function:

declare let a: Just<number>;

const add3 = (v: number) = > v + 3
add3(a) // ❌ parameters of type "Just
      
       " cannot be assigned to parameters of type "number"
      
Copy the code

Fmap is from the street, Fmap is hip to Contexts. Who else? Fmap knows how to apply a function to a value wrapped in context. You can use fmap for any type that is Functor; in other words, Functor defines fmap.

For example, imagine you want to apply add3 to Just 2. Use fmap:

Just.of(2).fmap(add3) // Just 5
Copy the code

💥 bam! Fmap shows us what it has achieved. But how does Fmap know how to apply this function?


What exactly is a Functor?

Functor is a typeclass in Haskell. Its definition is as follows:

In Typescript, a Functor is considered an arbitrary type that defines an Fmap. See how FMAP works:

  1. A Functor fa, such as Just 2
  2. Fa defines an fmap, which takes a function fn, such as add3
  3. Fmap until how to apply FA to FN, returns a Functor fb. fa is of the same type as fb’s wrapper context, e.g. Fa is Just, then fb is Just; Conversely, Fa is Nothing, fb is Nothing;

Describe it in Typescript function signatures:

<Functor T>.fmap<U>(fn: (val: T) => U).Functor U>
Copy the code

So here’s what we can do:

Just.of(2).fmap(add3) // Just 5
Copy the code

Fmap magically applies this function, because Maybe is a Functor that specifies how fmap applies to Just and Nothing:

class Just<T> {
  // ...
  Fmap / / implementation
  fmap<U>(fn: (val: T) => U) {
    return Just.of(fn(this.value))}}class None {
  // NoneAccept that any function returnsNone
  static fmap(fn: any) {
    return None}}Copy the code


Here’s what happens behind the scenes when we write Just.of(2).fmap(add3) :

So then, just like that, fmap, how about you apply add3 to Nothing?

None.fmap(add3) // Nothing
Copy the code

Like Morpheus in The Matrix, Fmap knows what to do; If you start with Nothing, you end with Nothing! Fmap is zen.

Now it tells us why the Maybe data type exists. For example, here’s how to handle a database record in a language without Maybe, such as Javascript:

let post = Post.findByID(1)
if(post ! =null) {
  return post.title
} else {
  return null
}
Copy the code

With FMAP:

// Suppose findPost returns Maybe
      
findPost(1).fmap(getPostTitle) Copy the code

If findPost returns an article, we get its title via getPostTitle. If it returns Nothing, we return Nothing! It’s a lot cleaner, right?

With Optional Chaining in Typescript, handling null can also be concise:

findPost(1)? .title// Same thing
Copy the code

The article also defines a version of the overloaded operator of fmap, which is briefly covered here because JavaScript does not support overloaded operators

GetPostTitle <$> findPost(1) // Use operator overload <$> to simplify fmap. Equivalent to the code aboveCopy the code


Another example: What happens if you apply a function to an Array (List in Haksell)?

Array is also functor!

[1.2.3].map(add3) // [4, 5, 6]. Fa is an Array, and output FB is an Array, which is Functor, so Javascript map is fmap and Array is Functor
Copy the code


Ok, ok, one last example: what happens if you apply a function to another function?

const multiply3 = (v: number) = > v * 3
const add3 = (v: number) = > v + 3

add3.fmap(multiply3) / / ❓
Copy the code

This is a function:

This is a function applied to another function:

The result is another function!

// Use examples only, do not imitate
interface Function {
  fmap<V, T, U>(this: (val: V) => T, fn: (val: T) => U) : (val: V) = > U
}
Function.prototype.fmap = function(fn) {
  return v= > fn(this(v))
}
Copy the code

So functions are Functor! For a function, fmap is compose! For example: compose(f, g) = compose(f, g)


Functor summary

A Functor is not that hard to understand, as we can see from the above example. A Functor is:

<Functor T>.fmap(fn: (v: T) => U): <Functor U>
Copy the code

Functor defines a ‘fmap’ operation that takes a function fn that receives a specific value and returns another specific value, such as add3 above. Fmap determines how to apply fn to the source Functor(a) and returns a new Functor(b). That is, fmap’s source and output value ‘context’ types are the same. Such as

  • Just -> fmap -> Just
  • Nothing -> fmap -> Nothing
  • Maybe -> fmap -> Maybe
  • Array -> fmap -> Array





Applicative

Now it’s double time. Applicative takes it to another level.

For Applicative, our value is still wrapped in the same context as Functor

The difference is that we also wrap functions in Functor (such as add3) in a context!

Well. Let’s go further. Applicative is not kidding. Unlike Haskell, Typescript has no built-in way to handle Applicative. We can define an apply function for the types that need to support Applicative. The apply function knows how to apply a context-wrapped function to a context-wrapped value:

class None {
  static apply(fn: any) {
    returnNone; }}class Just<T> {
  // Use method overloading to make Typescript inferential
  // If the value and function are of type Just, the result is also of type Just
  apply<U>(fn: Just<(a: T) => U>) :Just<U>; // If the function isNothingType, the result isNothing// Strictly speakingapplyOnly functions of the same context type should be received, i.eJustBecause, / /MaybeisTypescripttheUnionType, there is no way to extend methods to it, so here will beMaybeandJustIt's mixed upapply<U> (fn: Nothing) :Nothing; // If both the value and function areMaybeType, returns oneMaybetypeapply<U> (fn: Maybe<(a: T) => U>) :Maybe<U> {
    if (! isNothing(fn)) {
      return Just.of(fn.value(this.value));
    }
    return None.apply(fn); }}Copy the code


Let’s look at arrays:

// For example only
interface Array<T> {
  apply<U>(fns: Array< (e: T) => U>) :U[]} // Accept a function array (contextReturns a new array with 'functions' appliedArray.prototype.apply = function<T.U> (fns: Array< (e: T) => U>) {
  const res: U[] = []
  for (const fn of fns) {
    this.forEach(el => res.push(fn(el)))}return res
}
Copy the code


In Haskell, use <*> to denote the apply operation: Just (+3) <*> Just 2 == Just 5. Typescript does not support operator overloading, so ignore it.

Applicative of type Just

Array type Applicative

const num: number[] = [1.2.3]
console.log(num.apply([multiply2, add3]))
// [2, 4, 6, 4, 5, 6]
Copy the code

There are things that Applicative can do that Functor can’t. How do I apply a function that takes two arguments to two wrapped values?

// a curry-type addition function that supports two arguments
const curriedAddition = (a: number) = > (b: number) => a + b

Just.of(5).fmap(curriedAddition) Return 'Just. Of ((b: number) => 5 + b)'
/ / Ok to continue
Just.of(4).fmap(Just.of((b: number) = > 5 + b))  Functor can't handle fn wrapped in context
Copy the code

But Applicative can:

Just.of(5).fmap(curriedAddition) Return 'Just. Of ((b: number) => 5 + b)'
// ✅ dangdang dang
Just.of(3).apply(Just.of((b: number) = > 5 + b)) // Just.of(8)
Copy the code

“Applicative pushed Functor aside. “Big can use functions with any number of arguments,” it says. “Armed with <$>(fmap) and <*>(apply), I can accept any function with any number of unwrapped value arguments. Then I pass it all wrapped values, and I get a wrapped value out! Aaargh!”

Just.of(3).apply(Just.of(5).fmap(curriedAddition)) / / return ` Just. Of ` (8)
Copy the code


Applicative summary

Let’s restate an Applicative definition. If Functor requires fMAP, Applicative means apply, which meets the following definition:

// This is the fmap definition of Functor <Functor T>. Fmap (fn: (v: T) => U): <Functor U> // This is the apply definition of Applicative. In contrast, fn becomes a context-wrapped function <Applicative T>. Apply (fn: <Applicative (v: T) => U>): <Applicative U>Copy the code





Monad

Three days of practice at last! Continue ⛽ come on ️

How to learn Monad:

  1. You need a PhD in computer science.
  2. Then throw it away, because you don’t need it in this article!

Monad adds a new twist.

Functor applies a function to a wrapped value:

Applicative applies a wrapped function to a wrapped value:

Monad applies a function that returns a wrapped value to a wrapped value. Monad defines a function flatMap (which in Haskell uses the >>= operator to apply Monad, pronounced “bind”) to do this.

Let’s look at an example. Old partner Maybe is a Monad:

Suppose half is a function that applies only to even numbers:

// This is a typical "return wrapped value" function
function half(value: number) :Maybe<number> {
  if (value % 2= = =0) {
    return Just.of(value / 2)}return None
}
Copy the code

What if we feed it a packaged value?

We need to use flatMap(>>= in Haskell) to stuff our wrapped values into this function. Here’s a picture of >>= :

Here’s how it works:

Just.of(3).flatMap(half) // => Nothing, Haskell uses the operator Just 3 >>= half
Just.of(4).flatMap(half) // => Just 2
None.flatMap(half)       // => Nothing
Copy the code

What’s going on inside? Let’s look at the flatMap method signature again:

// Maybe
Maybe<T>.flatMap<U>(fn: (val: T) => Maybe<U>) :Maybe<U>

// Array
Array<T>.flatMap<U> (fn: (val: T) => U[]) :U[]
Copy the code

Array is a Monad, Javascript Array flatMap has officially become standard, take a look at its use examples:

const arr1 = [1.2.3.4];
arr1.map(x= > [x * 2]); 
[[2], [4], [6], [8]]

arr1.flatMap(x= > [x * 2]);
// [2, 4, 6, 8]

// only one level is flattened
arr1.flatMap(x= > [[x * 2]]);
[[2], [4], [6], [8]]
Copy the code


Maybe is also a Monad:

class None {
  static flatMap(fn: any): Nothing {
    returnNone; }}class Just<T> {
  // Same as apply above
  // Use method overloading to make Typescript inferential
  // If the function returns Just, the result is also Just
  flatMap<U>(fn: (a: T) => Just<U>) :Just<U>; // If the function returns a value ofNothingType, the result isNothing.
  flatMap<U> (fn: (a: T) => Nothing) :Nothing; // If the function returns a value ofMaybeType, returns oneMaybetypeflatMap<U> (fn: (a: T) => Maybe<U>) :Maybe<U> {
    return fn(this.value}} // exampleJust.of(3).flatMap(half) / /Nothing
Just.of(4).flatMap(half) / /Just.of(4)
Copy the code

This is the case with Just 3 working!

It’s even easier to pass in Nothing:

You can also concatenate these calls:

Just.of(20).flatMap(half).flatMap(half).flatMap(falf) // => Nothing
Copy the code


It’s cool! So now we know Maybe is a Functor, Applicative, or Monad.

The article also demonstrates another example: IO Monad, which we’ll look at briefly here

The IO signature is as follows:

class IO<T> {
  val: T
  // We don't care about the implementation yet
  flatMap(fn: (val: T) = > IO<U>): IO<U>
}
Copy the code

Let’s look at three functions. GetLine has no arguments and is used to get user input:

function getLine() :IO<string>
Copy the code

ReadFile takes a string (filename) and returns the contents of the file:

function readFile(filename: string) :IO<string>
Copy the code

PutStrLn prints a string to the console:

function putStrLn(str: string) :IO<void>
Copy the code

All three of these functions take a normal value (or no value) and return a wrapped value, IO. We can concatenate them using flatMap!

getLine().flatMap(readFile).flatMap(putStrLn)
Copy the code

That’s great! Take your seats in the front row for monad’s presentation! We don’t need to waste time unpacking and repacking IO Monad values. FlatMap does that for us!

Haskell also provides syntactic sugar for Monad, called do expressions:

foo = do
    filename <- getLine
    contents <- readFile filename
    putStrLn contents
Copy the code


conclusion

  1. Functor is implementedfmapThe data type of.
  2. Applicative is implementedapplyThe data type of.
  3. Monad is implementedflatMapThe data type of.
  4. Maybe does all three, so it’s functor, Applicative, and monad.

What’s the difference between the three?

  1. functor: One can be obtained via fmapfunctionApply to aWrapped valuesOn.
  2. applicative: You can apply oneWrapped functionsApplied to theWrapped valuesOn.
  3. monad: One can be obtained via flatMapA function that returns a wrapped valueApplied to theWrapped valuesOn.

Take a look at their signatures together:

// This is the fmap definition of Functor <Functor T>. Fmap (fn: (v: T) => U): <Functor U> // This is the apply definition of Applicative. In contrast, fn becomes a context-wrapped function <Applicative T>. Apply (fn: <Applicative (v: T) => U>): <Applicative U> // the definition of Monad, while accepting a function that returns a context-wrapped value <Monad T>. Flatmap (fn: (v: T) => <Monad U>): <Monad U>Copy the code

So, dear friends (I think we are friends now), I think we can all agree that Monad is a simple and SMART IDEA(TM). Now that you’ve moistened your whistle with this guide, why not pull on the Mel Gibson and grab the whole bottle. See See Some Monads in the Haskell Guide to Fun Learning. There’s a lot of stuff THAT I’m actually hiding because Miran does a really good job of getting into it.


extension

On the basis of the original text, this paper refers to the following translated versions, thanks again to these authors:

  • Functors, Applicatives, And Monads In Pictures – original Text
  • Swift Functors, Applicatives, and Monads in Pictures – Swift Version for this article
  • Kotlin layout solution Functor, Applicative and Monad-Kotlin version, translation is very good
  • Functor, Applicative, and Monad picture interpretation – Chinese version, title leaf translation
  • Your Easy Guide to Monads, Applicatives, & Functors-Medium gifs illustrating Monad are also well written. You can reread this article after you finish reading it