4

I finally got my head around monads once I understood how they are useful in c++/python by allowing you to chain together functions of the form T -> Generic<U>. For example, if you have

readfile = [](string s) -> optional<string> {...};
http_request = [](string s) -> optional<int> {...};
inverse = [](int i) -> optional<int> {...}

then bind >>= :: optional<T> -> (T -> optional<U>) -> optional<U> has exactly the right signature to allow you to compose these functions

optional{"myfile"} >>= readfile >>= http_request >>= inverse;
// or in haskell
read_request_inverse = (>>= inverse) . (>>= http_request) . readfile

instead of writing out the short circuiting conditionals by hand

[]() -> optional<int>
{
    auto message = readfile("myfile");
    if (!message)
        return nullopt
    auto number = http_request(*message)
    if (!number)
        return nullopt
    return inverse(*number)
}

I think what tripped me up was not distinguishing chained binds from nested binds (which Bartosz Milewski demonstrates with c++ ranges) and understanding them separately

auto triples =
  for_each(ints(1), [](int z) {
    return for_each(ints(1, z), [=](int x) {
      return for_each(ints(x, z), [=](int y) {
        return yield_if(x*x + y*y == z*z, std::make_tuple(x, y, z));
      });
    });
  });

which is just list comprehension

triples = [(x, y, z) | z <- [1..]
                     , x <- [1..z]
                     , y <- [x..z]
                     , x^2 + y^2 == z^2]

Also I believe I got tripped up on the fact that the reader, writer, and state monads are just trying to reverse engineer side-effects (which I'm not entirely sure doesn't reintroduce the problems of impure procedures/subroutines) and so aren't useful in multi-paradigm languages where you can just have real (sensibly constrained) side-effects.

So monadic optional<T>/result<T,E> seems highly useful in c++/python/rust, and monadic ranges/lists might be useful solving coding challenges but not really for real life problems (edit: I've actually started using flatmap/views::for_each it a lot for simplifying "business logic").

So my question is are there any other examples of monads that can justify the usefulness of monads in multi-paradigm languages?

Tom Huntington
  • 2,260
  • 10
  • 20

2 Answers2

3

Programming languages that make the most use of monads are, of course, functional programming languages. You are using Haskell in your question, but monads are also useful in OCaml.

A first example of this programming pattern being useful is in Nix. Nix is a package manager, that uses scripts written in nix (yes, there's a bit of overlap with terminology) to describe packaging, as well as other things. If you were wondering, yes, nix is a pure, lazy functional programming language. It goes as far as having an entire OS whose configuration is written in nix, called NixOS. The monad patterns appears quite often when you package applications, although in a quite convoluted way. The idea is that packages themselves are monads, having a functor that allows you to chain modifications of the package. In pseudo-code, keeping your notation, it could be something like package >>= apply_patch >>= change_version >>= add_tests.

An other example is in Rust. Contrary to Haskell, OCaml or nix, Rust is not a functional progrmaming language (more of a system programming language), but it has many aspects of a functional programming language, and many of their patterns, including the option one, for instance, which is, in Rust, called Option<T> (being generic over T). Rust provides several methods besides the basic map functor to deal with Option<T> objects, so typical Rust code might look like (assuming a: Option<T>)

a
  .map(|x| ...)
  .filter(|x| ...)
  .and_then(|x| ...)
  .or_default()

However, Rust does not entierly deals with monads in the same way Haskell, say, would deal with them, because in Rust it's quite common that a function that is monad-compatible (ie. does_something: fn(U) -> Option<T>) will actually unpack the monad-contained values, operate directly on that value, and then rewrap them. This is made simple with the ? operator, which is just syntax sugar for a macro (the try! one). The usage is the following, if a: Option<T>.

fn do_something_with_a(a: Option<T>) -> Option<U> {
  let b = do_something_with_a_value(a?);
  Some(b)
}

Here, the ? operators acts by first matching over a. If it's a None, then the function immediately returns None. Otherwise, it's a Some(x), and x is returned. That is, this ? operator acts very much like >>=, except that it does not take a closure, and run it on x if a is Some(x). Instead, it will make as if the current function was that closure, by letting it operating directly on x.

So far, so good. But Rust has other predefined types that have this monadic pattern, which are not in your original question. For instance, Result<T, E>. In Rust, you usually want to avoid throw, to panic!, to raise, or however you want to call it. This is not how exception management is done. Instead, if a function that returns a type T could throw an error, of type E, then it should instead return Result<T, E>. It is (as you might imagine) simply defined as an enum, just like Option<T> was:

enum Result<T, E> {
  Ok(T),
  Err(E),
}

and, just like Option<T>, Rust provides a basic functor to act this type, which is map (ie. my_result.map(|x| ...)). However, just like Option, it also provides plenty of other methods to work with Result objects, but there is one way that is particularly idiomatic: the ? operator (yes, again)! Just like with an option, what is does is the following:

  • if the object is of the form Ok(x), then its value is x;
  • otherwise, it's of the form Err(y), and it will return from the current function with Err(y).

Actually, ? is even a bit smarter than that because your function might call several functions that return Result<_, _>, but with different error-types. For instance, if you try to read from a file, you may get an IO error. Then you try to parse some data, and you may get a parser error. Then you do some arithmetic and you may get an arithmetic error. To deal with that, your function should return a generic-enough error so that ? may perform conversion between the errors that you may get (IO, parse, arithmetic, ...) and the one that you return.

jthulhu
  • 7,223
  • 2
  • 16
  • 33
  • Thanks I didn't know about `?`. It lets you define `(>>= http_request)` rather than `http_request` itself. Would be interesting to compare the pros and cons. However, my question really was are their any useful monads that aren't `optional`/`result` (they are basically the same monad) – Tom Huntington Apr 18 '22 at 07:03
  • I didn't realise that nix was using monads. So the override pattern and fixed point evaluation is monadic. Would be interested if you formulate `package >>= apply_patch >>= change_version >>= add_tests` in the terms of this blog post http://blog.tpleyer.de/posts/2020-01-29-Nix-overlay-evaluation-example.html – Tom Huntington Apr 18 '22 at 07:10
  • 1
    @TomHuntington to formulate it with overlays and the fix-point operator, `>>=` is the application of an overlay, `apply_patch` is an overlay, and the fix-point operator is the "extraction" of the monad, that is, once you have done your computation, you may want to extract the value from the monad (ie. think of it as when you want to actually get a value from an `Option`) by computing the actual resulting package. – jthulhu Apr 18 '22 at 07:27
  • 1
    Also, even though `Option` and `Result` use the same convenient macro `?`, they are not the same monad. It might seem a little be deceiving, because one may expect cool and fancy and complicated monads, but the one that are the most useful (and thus the most used) are, from a theoretical standpoint, quite elementary. You won't find much extensively-used monads that are very far from `Option` or `Result`. One exception is Nix, whose monads are much more convoluted. – jthulhu Apr 18 '22 at 07:30
  • I still think they are basically the same monad, they enable the same short circuiting behaviour – Tom Huntington Apr 18 '22 at 07:32
  • But I think the nix monad would be useful in multi-paradigm languages, so that kind of answers the question. Yes, there are other useful monads but they'll be more complicated and for more specific situations – Tom Huntington Apr 18 '22 at 07:35
  • @TomHuntington if you want more examples of "real world monads", you should probably look into Haskell. In that language, a lot of objects are seen as monads (such as linked lists, in addition to the classic `Option` and `Result` haskell equivalent) and a lot of code patterns rely on monads (IO for instance). – jthulhu Sep 26 '22 at 07:05
  • I have looked into haskell and decided that a lot of their monads do nothing more than enable imperative code, which is ridiculous as is do notation (although it's necessary to get the `invocable |> east` vs `invokable(west)`) . Monads that don't solve actual problems are harmful (Reader, Writer ect.) – Tom Huntington Sep 27 '22 at 20:33
  • @TomHuntington this seems like a very reductive point of view. Sure, if you just looked at Haskell for the first time you might be weird out: first, they tell you it's "pure and all", then you wonder how they do deeply-imperative things like IO, and you see they resort to a `do` which looks very much like what you would do in eg. Python, except more contrived because you actually need to understand monads to understand what is going on. – jthulhu Sep 27 '22 at 20:37
  • @TomHuntington except that these monads (Reader, Writer just to mention these) are extremely elegant solution to a very deep problem. So deep that its formalization as mathematical objects requires re-building the mathematics based on a logical system called "type theory" (as opposed to first order logic combined with set theory, for instance). Reader and Writer are literally the embodiment of an abstract and powerful solution to an actual problem. – jthulhu Sep 27 '22 at 20:41
  • @TomHuntington and you have to understand that the `do` block in Haskell is *much more powerful* than imperative code. Its greatest strength is that it looks like imperative code, in a language where imperative code *does not exist*, so you can still do every optimization & reasoning about Haskell code that is usually completely impossible to apply on imperative code. – jthulhu Sep 27 '22 at 20:43
  • I understand haskell, and I program a lot in c++ ranges, so I understand how powerful monads are (an am eager for monadic optionals) and can compare the two. I think do notation is bad. It muddles applicatives and monads (the difference should be explicit and syntactic). It makes functional code look imperative. It hides that you are writing a lambda. The one thing it does right is "flip"/"reverse" the arguments to bind to allow writing code top to bottom rather than right to left. – Tom Huntington Sep 27 '22 at 21:00
  • I think it would be better to teach monads in python. And only teach the ones that solve actual problems in multi-paradigm languages – Tom Huntington Sep 27 '22 at 21:03
  • When I was learning haskell I got terribly confused between the implementation of bind and function passed to it. Probably because both use do notation. It would have been better to just teach `flat_map` for List/Maybe/Future and not gone into implementations – Tom Huntington Sep 27 '22 at 21:09
  • To understand monads, you only need to know how to call bind not implement it, although this is beside your point – Tom Huntington Sep 27 '22 at 21:49
1

React tries to be a comonad

React Suspense is to a Monad as Hooks are to Applicative Notation

Lazy computations are monadic

Futures are monadic

ReactiveX observables are a combination of futures and iterables.

Tom Huntington
  • 2,260
  • 10
  • 20