Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Monads aren't as hard as you think (yingw787.com)
40 points by yingw787 on Dec 6, 2019 | hide | past | favorite | 23 comments


I find these introductory articles ultimately misleading. Monad is never hard, just like group is never hard, vector space is never hard, lattice is never hard, manifold is never hard, and on and on. What's hard is to understand not just one single concept but layers of webs of abstract concepts, along with all the theorems and ingenious ways of applying them. What turns people away is not the difficulty of monad or functional programming in general, but lack of incentives. Why don't we just be upfront and tell people: XXX is hard, but you know what? If you master it, here is what you will get out of it.


@hintymad thanks for sharing! For me, monads seemed hugely difficult to learn, and so I never got started with them at all. Had I just known that they were not as hard as I thought they were (which I think is different from not hard) I might have started earlier. I’m hoping that by sharing some of my understandings I can help people avoid some of my own hesitation.

What I really like about the HN community is the notion of shipping even if you’re embarrassed by it, because the future good or bad is built by the people who show up.


It only seems not-hard for people that already understand them. Here's where the post shows just how simple monads can be.

> A monad is a data type (e.g. int) that encapsulates some control flow (e.g. try/catch).

But... I'm already lost. How would an integer "encapsulate" control flow? Like different integer values would represent different errors that could occur? Or you could use integers as a representation for a language that has try/catch as a feature? Even in this simplest case, if you don't already grok monads, it's somehow not illuminating for me. YMMV


that's just poor wording -- the first example wasn't supposed to be linked to the second example;

A better sentence is probably

A monad is a datatype (e.g. Either<T,U>) that encapsulates some control flow (e.g. if T != null return T else U)

if I understand it correctly


Because telling someone "XXX is hard but you should learn it" doesn't actually get the person closer to an understanding of XXX.

Telling something "XXX is really useful and you've been putting off learning it because most of the material is inaccessible, so here's a gentle introduction to the concepts" does, though.


So what will I get out of it once I master it?


This is one of my favorite songs about Monads: https://www.youtube.com/watch?v=BoJGIqyriCc&list=PLw0jj21rhf...


@rdegges thanks for sharing!


> A monad is a data type (e.g. int) that encapsulates some control flow (e.g. try/catch).

Ignoring the fact that `int` itself doesn't have much to do with monads (it's presumably just giving an example of a type), I've always disliked the notion of talking about monads being types or vice versa.

The way I think of it is that the monad itself consists of the monadic operations for some set of values. The "IO monad" consists of the `bind` and `unit` functions to do with values of types `IO<T>` for any `T` (eg, `IO<Int>`, `IO<String>`, `IO<IO<Int>>`). Similarly, the "Maybe monad" consists of the `bind` and `unit` functions to do with values of types `Maybe<T>` for any `T`.

Both of these monads can be said to implement a common interface, which might be expressed using the following Java-like code (using Java just to emphasise that there isn't really[0] anything magical here):

  interface Monad<M> {
    <T> M<T> unit(T v);
    <T, U> M<U> bind(M<T> a, Function<T, M<U>> f);
  }
If we were to define an "IO monad", we would expect it to implement `Monad<IO>`, therefore an "IO monad" is really just a value of type `Monad<IO>`:

  Monad<IO> ioMonad = new Monad<IO>() {
    <T> IO<T> unit(T v) { .. }
    <T, U> IO<U> bind(IO<T> a, Function<T, IO<U>> f) { .. }
  };
This is actually pretty much exactly how it works in Idris for example, where `Monad IO` is itself a type, and you can actually ask for the `Monad IO` implementation by writing, funnily enough, `the (Monad IO) %implementation`:

  Idris> 4 + 5 -- to demonstrate the REPL
  9 : Integer
  Idris> Monad IO
  Monad (IO' (MkFFI C_Types String String)) : Type
  Idris> the (Monad IO) %implementation
  constructor of Prelude.Monad.Monad (\meth, meth, meth, meth => io_bind meth meth) (\meth, meth => io_bind meth id) : Monad IO
[0] The only reason it doesn't actually work is because Java doesn't support higher-kinded polymorphism, so you can't pass type-level functions such as `IO` or `List` as type parameters, therefore you can't actually write `M<T>` as appears in the interface body.


> The only way to really understand a complex concept is to see it from as many different angles as possible.

- Bartosz Milewski


So a simple div function (x/y) turned into several classes and boilerplate. For what again? So that the program would not crash if it got into a bad state? JavaScript please give me back early errors! I hate that Promises just swallow errors. If there is an unexpected state, something that should never happen,then GTFO immediately, print the stack, give me a friendly error with a line nr to make it simple to debug, create a memory dump. Don't continue the execution with a poisoned state! If you want a safe program, it should exit at even the slightest smell of error. Not make my heart beat 300 bpm or put the airplane into a nose dive, or show the wrong altitude so that the plane flies into a mountain. So you are feeling anxious that your program can throw an error at any time? Don't be, that's much better then the alternative described with the airplane above. You solve the edge cases, and gracefully handle errors that can be worked around, but for unexpected errors, like the altitude showing -2 million. (int overflow or something) you want it to crash (the program, not the airplane).


Promises don't swallow errors, they pass it up to whatever next promise gets chained or any .catch() handlers registered. Browsers have an event to listen to for unhandled promise rejections at the global level for all those places you fired off a promise but forgot to do anything with it. (All the major browsers now show error messages and stack traces in the dev console as a default action for all unhandled promise rejections. They don't stop page execution as browsers decided against that back in the early IE/Netscape era to be more graceful in a complex event-based world. Node also spits out unhandled promise rejections to the console and hard quits a process now.)

But the juicy benefit to Promises is the (monad-based) transformation to async/await notation. Async/await notation gives you back the try { } catch { } synchronous-looking error catching that you are wishing for, all it takes is a couple new magic keywords and your code looks very similar to what it would in a magic synchronous world without promises. (Most current browsers and current versions of Node even directly support async/await today [0] directly out of the box, no need for a compiler/transpiler tool to do the transformation for you.)

[0] https://caniuse.com/#feat=async-functions


In JavaScript there are two types of errors: First class errors that you can return, pass around and bubble up. And then there are exceptions that means the program did something unexpecting. Promises make no exception and does not treat them as first class citizens.


Promises make future exceptions. If you aren't paying attention "in the future" to catch those exceptions, they bubble to an error handler (window.onUnhandledRejection [0]), so it acts as both types of JS errors, depending on how you want to pay attention to it.

async/await syntax lets you catch promise rejection exactly like you would any other type of JS exception with a try {} catch {}.

[0] https://developer.mozilla.org/en-US/docs/Web/API/Window/unha...


To bring things back on topic: async/await syntax in JS inherits its magic almost directly from Haskell's do-notation and is exactly an example of why exploring Monads has been practically useful for languages far outside of the "academic ivory tower walls" of Haskell.


Every error that happens after the promise have completed are treated as if they where exceptions to the Promise, even though they have nothing to do with what was promised, including type, syntax, and state errors.


Depends a bit on what your crash response strategy looks like eh...


The only monad anyone ever talks about in my hearing is the IO monad. I sometimes imagine all monads reduce to the IO monad and some specified side effect by intent.

Except of course the whole point of monadic thinking appears to be "no side effects" so .. I'm stuck


People talk a lot about the IO monad because it's one of the most complex monads in general use (and referred to as a monad by a major language and a lot of developers; there are a lot of things that can be considered monads, follow the monad laws, but no one bothers calling them monads).

Monads don't have anything to do with IO or avoiding side effects, they are about composing/combining operations. At heart, the monad laws basically state that if you have two puzzle pieces with similar edges you can snap them together and start thinking about them as a larger puzzle piece in the same puzzle, and that eventually when you are done snapping together pieces you can admire whatever image they form as a result.

The fact that people talk a lot about the IO Monad in Haskell was that it was a jigsaw puzzle that solved a lot of problems in early Haskell, and led to a lot of exploration in monads.


If you know any linear algebra here's an example that has nothing to do with side effects:

Let X be any set. Let mX be the set of formal linear combinations of elements of X (that is, expressions like 3x₁+2x₂ where x₁ and x₂ are elements of X). Then m is a monad.

fmap is variable substitution. Ex. fmap [x₁->y₁, x₂->y₂] (3x₁ + 2x₂) = (3y₁ + 2y₂).

join is simplification of nested expressions. Ex. join (3(2x₁) + 2(x₁ + 2x₂)) = (8x₁ + 4x₂).

>>= is substitution and simplification. Ex. (3x₁ + 2x₂) >>= [x₁->3y₂, x₁->-y₂] = (3(3y₂) + 2(-y₂)) = 7y₂.

A Kleisli arrow X->mY is a system of linear equations, ie. a matrix

    x₁ = 3y₁ + 6y₂
    x₂ =  y₁ - 2y₂
>=> is composition of linear systems, ie. matrix multiplication.

Whereas in IO you think of f >=> g as meaning "first f, then g", here f >=> g is the matrix product fg, ie. "first g, then f".


I was like if m in mX is a monad, how is m not a function? Or if it is a function, how is it different from functions in general?

Googling, I found the following helpful comment:

"...while addition and multiplication are both monoids over the positive natural numbers, a monad is a monoid object in a category of endofunctors: return is the unit, and join is the binary operation. It couldn't be more simple. If that confuses you, it might be helpful to see a Monad as a lax functor from a terminal bicategory"


Wait, I think I may have found a good explanation:

"A few months ago Brent Yorgey complained about a certain class of tutorials which present monads by explaining how monads are like burritos.

At first I thought the choice of burritos was only a facetious reference to the peculiar and sometimes strained analogies these tutorials make. But then I realized that monads are like burritos."

https://blog.plover.com/prog/burritos.html


You can do it, or explain it but you can't do both: proof see above




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: