←back to thread

A list is a monad

(alexyorke.github.io)
153 points polygot | 3 comments | | HN request time: 0.012s | source
Show context
kerblang ◴[] No.44447914[source]
The way I think of it, monads are a solution to Callback Hell, where you've fallen in love with lambdas, but now you have a nightmarish mess of lambdas in lambdas and lambdas calling lambdas. The monadic functions allow you to create "for comprehensions" aka "do comprehensions" but really, they look like a classic for-each loop. They secretly call the monadic map/flatMap/filter functions.

    for x in list
        doThings(x)
These comprehensions have a strange bonus feature, that you can do nested "loops" all at once, and even add "guards" (little if statements)

    newlist=
        for x in list1
            y in list2 if y > 3
            z in list3
            doThings(x, y, z)
But again, the comprehension, when "de-sugared", is secretly calling the map/flatMap/filter functions of list1, list2, list3 to get our result. You as the author of a given monad can implement those functions however you want, and they're all 3 lambda based. But notice how the comprehension is flattening those lambdas out! Our callbacks in callbacks are much more readable like this.

Without comprehensions, you can still implement monadic functions in any old language (probably in C...?), and they're handy in their own right, but you don't get the flattening-of-callback-hell magic.

replies(1): >>44447956 #
stronglikedan ◴[] No.44447956[source]
After reading your comment, I've made it my mission to understand it. Although I have no idea what you're talking about, you make it sound intriguing.
replies(6): >>44448147 #>>44448192 #>>44448734 #>>44449291 #>>44450891 #>>44452126 #
1. nine_k ◴[] No.44448734[source]
To get a minimal idea, you can think about a monad as of a parametrized class: M<T>. Its functioning follows "monad laws" that allow you to do certain things with it, and with the value(s) of T wrapped my it. In particular, you can always "map" the values:

  M<T1>::map(f: (T1 -> T2)): M<T2>
  List<int>([1, 2, 3]).map(x => toString(x)) == List<string>(["1", "2", "3"])
You can always flatten the nested structure:

  M<M<T>>::flatten(): M<T>  // [["a", "b"], ["c", "d"]] -> ["a", "b", "c", "d"]
This is usually expressed in a different form, more fundamental:

  M<T1>::flatMap(f: (T1 => M<T2>)): M<T2>
  List(["a b", "c d"]).flatMap(x => x.split()) == List(["a", "b", "c", "d"])
You can notice how that map() thing does looping over a sequence for you.

But Optional<T> is also a monad:

  let x: Optional<int> = Some(1);
  let y: Optional<int> = Nothing;
  x.map(n => n + 1).map(n => n * 2) == Some(4);
  y.map(n => n + 1).map(n => n * 2) == Nothing;
As you see, the same map() (and flatMap()) does the condition checking for you. and can be chained safely.

You can also notice how chaining of map-like operations does operation sequencing:

  fetch(url).then(content => content.json()).then(data => process(data))
Your language, like JS/TS, can add some syntax sugar over it, and allow you to write it as a sequence of statements:

  async () => {
    const response = await fetch(url);
    const data = await response.json();
    process(data);
  } 
Promises are not exactly monads though, a Promise<Promise<T>> immediately transforms into Promise<T>. But other monadic properties are still there.
replies(1): >>44451979 #
2. timewizard ◴[] No.44451979[source]
> immediately transforms into

Minor quibble, "can only be resolved as". The runtime absolutely holds Promise<Promise<T>>'s.

replies(1): >>44452165 #
3. nine_k ◴[] No.44452165[source]
Splitting hairs even further: the .then() returns a resolved value of the inner Promise, not the inner Promise itself, when the outer Promise resolves, so not "immediately" indeed. That's where the flattening occurs, AFAICT.