← Back to context

Comment by wat10000

14 days ago

> Do you understand "flatmap"? Good, that's literally all a monad is: a flatmappable.

Awesome! Now I understand.

> Technically it's also an applicative functor

Aaaand you've lost me. This is probably why people think monads are difficult. The explanations keep involving these unfamiliar terms and act like we need to already know them to understand monads. You say it's just a flatmappable, but then it's also this other thing that gives you more?

But words like "incapsulation" or "polymorphism" or even "autoincrement" also sound unfamiliar and scary to a young kid who encounters them the first time. But the kid learns their meaning along the way, in a desire to build their own a game, or something. The feeling that one already knows a lot, sort of enough, and it'd be painful and boring to learn another abstract thing is a grown-up problem :-\

  • Those words need definitions, but they can both be defined using words most people know.

    Casual attempts at defining Monads often just sweep a pile of confusion around a room for a while, until everything gets hidden behind whatever odd piece of furniture that is familiar to the person generating the definition. They then imagine they have cleared up the confusion, but it is still there.

    • Most engineers don't have too much trouble understanding things like List<T>, or Promise<T>, or even Optional<T>, which all demonstrate vividly what a monad does (except Promise in JS that auto-flattens).

      A monad is a generalization of all them. It's a structure that covers values of type T, some "payload" (maybe one, like Promise, maybe many, like List, maybe even none, like List or Optional sometimes). You can ask it to do some operations on these values "inside", it's the map() operation. You can ask it to do similar thing when operation on each value produces a nested structure of the same kind, and flatten them into one level again: this is flatMap(). This is how Promises are chained. The result is again a structure of the same kind, maybe with "payload" of a different type.

      This is a really simple abstraction, simpler than most GoF patterns, to my mind, and more fundamental and useful.

      1 reply →

I mean people need to be familiar with mathematics. In mathematics things form things without having to understand them.

For example, The natural numbers form a ring and field over normal addition and multiplication , but you don't need to know ring theory to add numbers..

People need to stop worrying about not understanding things. No one understands everything.

  • Now imagine if every single explanation of natural numbers talked about rings and fields. Nobody ever just says "they're the counting numbers starting from one." A few of them might say, "they're the counting numbers starting from one, and they form a ring and field over addition and multiplication." And I might think, I understand the first part, but I'm not sure what the second part is and it sounds important, so maybe I still don't know what natural numbers are.

    I'm not worried, but it's amusing to see this person say it's so simple, and then immediately trample on it.

    • Well most people explain monads for no reason. I'm probably one of the rare Haskell developers who never explains them to anyone. It has nothing to do with IO.

      If someone is concerned with how to do IO in a pure language then I show them how it actually happens in GHC, which is via the type system enforcing only one instance of RealWorld# is alive at once. There is ABSOLUTELY nothing you need to know about monads to understand IO in Haskell. It's just function composition and careful use of case to force the evaluation of a token of type RealWorld#. Nothing magic about it.. you're just passing the state of the world around.

  • I get your point, but the natural numbers do not form a ring or field.

    • Sorry you're right. I was tired commenting.

      S/naturals/rationals/g for rings

      and s/naturals/algebraic numbers/g for fields

I'm sorry, I wasn't clear. The "technically" was meant to signal "it doesnt' matter to you, but to pedants here who get off on saying "well ACKshually": I didn't forget that, it's just not relevant :D

If you want a little more elucidation, what you need to know, unless you're aiming to be functional programming god, is that:

- a monad is a FLATMAPPABLE - all monads are also applicative functors, which i will explain last bc it's kind of a twist on MAPPABLE - all applicative functors, and thus all monads, are functors, which are MAPPABLEs - an applicative functor is essentially a mappable for functions that take more than one parameter

I think applicative functors are the hardest to grok because it's not immediately obvious why they're necessary. The type signature is strange, and it's like "why would I ever put a function inside a container??" I wrote a lot of functional code in Kotlin and TypeScript before I finally understood their utility. The effect of this was that a lot of awkward code became much cleaner.

So let's begin with functor (i.e., a mappable):

Container<Integer>

if you have a function Integer to Text, a functor allows you to convert the Integer to Text using a function called `map`. We do this with arrays all the time in Python, JavaScript, etc. It's a very familiar concept, but we don't call it "functor" in those languages.

BUT, what if you have

Container<Integer>

and the function you want to map with takes two parameters. A classic example is you want to use the Integer as the first argument of a constructor. Let's say Pair.

So if Pair's constructor is: a -> a -> (a, a), you would first map Container<Integer> with PairConstructor. Now you have Container<Integer -> (Integer, Integer)>.

To pass in the second Integer to finish constructing the tuple, you use the special property of applicative functors. This is often called "ap" (like "map" without the "m").

---

Now, I would say the ACTUAL most important thing about applicative functors is this:

Imagine if you had a list of words. You want to make an API call for each word. API calls are often modeled with the Async monad (which is also, as mentioned above, definitionally an applicative functor).

But if you mapped [Word] with CallApi, you would end up with [Async ApiResult]. This models "a list of successful and unsuccessful API calls."

But what if you wanted Async [ApiResult] instead? (One might say this is an attempt to model "all api calls successful, but if one api call fails, the whole operation is considered a failure."

This is where applicative functors shine: pulling the applicative functor out of the container and wrapping the whole container. (There's more cool stuff to learn about the nature of this "container" but that'd be for another lesson, much like how you don't learn about primitives and interfaces on the same day in an OOP class.)

Recall that constructing a list of N items would be

a -> a -> a -> ... -> a (n times) -> [a]

That looks an awful lot like one MAP followed by (n-1) APs, based on the discussion above! And that's exactly what it is.

You can map the first api call and then ap the rest, and you end up going over the entire list, getting Async [ApiResult].

Now, there are a lot of ways languages go about solving this kind of "fail if one of the operations fails rather than compile a list of all successes and failures."

But the nice thing about using Functors, Monads, etc. is that you have a bunch of functions that work on these things, and they handle a ton of code so you don't have to.

That collection of Words above? It's a list. Lists are a Traversable, and all Traversable have the following function:

traverse: (a -> Applicative b) -> Traversable a -> Applicative Traversable b

The above, the traversable is a list and applicative functor was apiCall, so your code is as simple as

traverse apiCall listOfWords

No juggling around anything. That's it. You know your result will be "list of successful results, or a failure."

---

There are many more of these "type classes," and the real power comes from not needing to write a lot of code anymore because it's baked into the properties of the various type classes. Have a type that can be mapped to an order able type? Bam, now your type is order able and you never have to write a sort function for your type. Etc.