Why are monads important

I’m learning the language. I was wondering why are monads important or not?

3 Likes

Googling around for tutorials and explainers on monads you’re sure to find an embarrassment of riches. But, for a tutorial explanation that has connection with the OCalm community, and which I personally found very illuminating, I’d recommend the Std (monads.Monads.Std) tutorial (previously announced in the post [ANN] Monads - the missing monad transformers library).

A tl;dr of the point of view presented in that tutorial is: monads are important because they let you parameterize your computational model in a pretty elegant way.

8 Likes

Imho, if you’re a learner, monads are not important. Or, they are important in the same way that certain design patterns are important in OOP. For a lot of programming in OCaml, monads are unnecessary. And the future direction of the language (with algebraic effects) will make them even more so.

1 Like

There are many different schools of thought on how to program in “higher-order imperative languages” like OCaml. One school of thought is to eschew all imperative features, and use monads, which are a recoding of those imperative features using a particular coding style. The most common such imperative features which some people eschew are I/O (and concurrency), exceptions, and state (imperative update). All of these features have implementations in OCaml directly, but … well, there are reasons one might have for choosing to eschew those implementations.

There are vehement differences of opinion as to whether one should or should not use monads instead of (for instance) native threads. I would suggest two things:

  1. the biggest reason to use monads, is if some library you want to use, is coded using monads.
  2. if you plan to use monads, it is very, very, very much worth learning them carefully and thoroughly; it is very easy to make mistakes with them, and understanding how to program in proper monadic style is worth spending time on.
4 Likes

I recommend this book in particular, it has OCaml flavor: Category theory for Programmers book - OCaml flavor - #4 by XVilka

Be sure to check the latest pre-release: Release Version 25 (0af503a) · hmemcpy/milewski-ctfp-pdf · GitHub

6 Likes

The monad is a pattern that naturally arises in functional everyday programming. Even if it weren’t discovered, you will be still using it every day, just without knowing its fancy (and useless) name. Here are some examples.

Suppose you’re writing a program that has some configuration data, like command-line parameters, and you want to pass it to some of your functions that depend on it. So you have a bunch of functions that has type config -> 'a and you constantly threading this annoying config from the caller to callee, e.g.,

let main ctxt = 
  let x = do_one_thing ctxt in
  let y = do_another_thing x ctxt in
  x + y

As soon as we see a repetitive pattern, we should look for an abstraction opportunity. In this case, our abstraction is called the reader monad and has type type 'a reader = config -> 'a, so we can implement corresponding bind operation and write,

let main = 
  let* x = do_one_thing in
  let+ y = do_another_thing x in
  x + y

Next, suppose you’re writing code that threads the state, i.e., unlike the previous example, your functions may functionally update the passed configuration, and now instead of writing ugly code like,

let state,x = do_one_thing state in
let state,y = do_another_thing x state in
x + y

We can write a much more readable and less error-prone version, which uses the
type 'a state = config -> 'a * config monad,

let* x = do_one_thing in
let+ y = do_another_thing x in
x + y

Now, let’s add another feature to our program. We’ve figured out that our functions can fail, so in addition to changing the state, we might also return an error. In this case, writing in the direct (non-monadic) style is even more painful and nearly impossible, e.g.,

match do_one_thing state with
| Error _ as err -> err
| Ok (state,x) -> match do_another_thing x state with
  | Error _ as err -> err
  | Ok (state,y) -> x + y

But in the monadic style it is still… you probably already guessed it,

let* x = do_one_thing in
let+ y = do_another_thing x in
x + y

So nothing changes, again. Notice how we were adding various effects and we didn’t need to change our business logic code. It is because we found the right abstraction. And this is the main power of monads, is that you’re able to write clear generic code and extend it without modification. This separation of concerns significantly reduces the cognition burden and you can focus on the business logic and forget about the implementation details. Thus monads enable you to tackle the more complex problems. That is especially true when you write parser-like code when you need backtracking and multiple choices.

But let’s return to our examples. You can see that the monadic version of the code is the same, the only thing that changes is the let* operator, which stands for the bind function (and let+, which stands for map, but is easily derivable from bind). This let* is the essence of the monad, as the monad is just an abstraction of the computation, i.e., it defines how terms are computed. And by abstracting it, or let’s say, reifying it into the let* operator, we can write generic code that doesn’t depend on how the computation unfolds. You can also think of the built-in let operator of OCaml as the monad, as well. It also offers error handling and state, but is less general (no backtracking, non-determinism) and, the main problem, too invasive. All computations are naturally embedded into the OCaml monad, so we can’t really say which computation is pure and which is not from its type. Unless we embrace some discipline and refrain from using the OCaml monad and stick to explicit monads.

To summarize, a monad is just an algebra of computations. We have int that is an algebra of integers, we have Set that is the algebra of sets. And we have the algebra of monads, which captures the idea of computation, i.e., evaluating a term and binding it to a result, i.e.,

type 'a t
val return : 'a -> 'a t
val (let*) : 'a t -> ('a -> 'b t) -> 'b t

Computations are so natural in our everyday programmer’s life, that we don’t even notice them, and don’t think about them as abstractions, thinking that it is the responsibility of the programming language abstract machine to carry computations for us. It turns out, that if we will take this power from the language and put it under our control, we can make wonders.

Algebraic effects will enable more efficient and straightforward implementations of monads, especially those that deal with non-pure effects, such as the IO monad. But by no means do they substitute the concept of the monad. In other words, the effects system is the implementation details, like algebraic data types, records, exceptions, etc. Not to be confused with the monad, which is an abstraction that can be implemented with effects, ADT, functions, etc.

26 Likes

This is an excellent beginning[1] to the argument for using monads! And the book mentioned is probably a good one to read to get more.

[1] not complaining about that: just noting it. Obviously there’s a ton more to the arguments, and you correctly point at various places one can go for that backup.

Yawar, I’m curious:

OCaml has historically been nonspecific about evaluation order of arguments in applications, expressions in tuples, etc. With algebraic effects, will that change? Or are algebraic effects specified in such a way as to preserve the compiler-writer’s ability to be nonspecific about evaluation order ?

I think the best way for a beginner to obtain an appreciation of monads in the first instance is to forget the theory and instead to use them: they are a practical tool or design pattern for threading some additional quality through a computation. Because ocaml has mutable state and exceptions they are perhaps less important than in pure languages, but they can still be useful in ocaml.

Simple monads which you are likely to come across in ocaml are the Option or Result monad and the List monad. Try playing around with Option.t and Result.t types and using them as an alternative to exceptions to represent a failure condition arising in a computation, including playing around with the >>= and let* bind operators (let ( >>= ) = Option.bind and let ( let* ) = Option.bind) and similarly with Result.bind. The bind operator for lists is List.concat_map: observe how it can be used to enable a mapping function to return multiple values or an empty value, so encapsulating different outcomes - try implementing a filter function using List.concat_map. Notice the similarity of signature of Option.bind compared with List.concat_map (save that the argument order of List.concat_map is reversed in ocaml).

When you have got the hang of that, try using the concurrency library Lwt. It’s basic type, Lwt.t, is a promise which is used in implementing the concurrency monad - something which may be fulfilled (or fail) at some time in the future. Observe the signature of Lwt.bind, and its resemblance to Option.bind and List.concat_map. I think it is at that point, when you have discovered how simple and easy to use they are, that you can dive into more of the theory.

5 Likes

Hi Chet, I wouldn’t know, perhaps the effect handler people can chime in. I haven’t heard anything to the effect (pardon my pun) though.

Now I’m confused, because I was also under the impression that effects can be used without monadic code and this was one of the benefits since straight-line code is simpler. I got this impression from here:

The multicore version of OCaml adds support for “effects”, removing the need for monadic code here. Using effects brings several advantages:

It’s faster, because no heap allocations are needed to simulate a stack.
Concurrent code can be written in the same style as plain non-concurrent code.
Because a real stack is used, backtraces from exceptions work as expected.
Other features of the language (such as try … with …) can be used in concurrent code.

2 Likes

This is more a matter of taste, at this point. Some people prefer the
purely functional world, which means monads are pervasively used; some
people like the more balanced approach of ML with loops, exceptions, and
blending imperative and functional together. Runtime effects make the latter
easier to use in the general case, even for highly concurrent programs
where Lwt is the norm today. Type-level effects will (some day) make
runtime effects as type safe as monads, but also more convenient (no
transformer stacks).

3 Likes

Thanks @c-cube, that clears up the confusion for me. I also appreciate your general explanations of monads, @ivg.