--- title: "A non-mathematician's introduction to monads" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{A non-mathematician's introduction to monads} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>" ) ``` ```{r setup, include = FALSE} library(chronicler) library(testthat) ``` # Introduction This vignette introduces the functional programming concept of *monad*, without going into much technical detail. `{chronicler}` is an implementation of a logger monad, but in truth, it is not necessary to know what monads are to use this package. However, if you are curious, read on. A monad is a computation device that offers two things: - the possibility to decorate functions so they can provide additional output without having to touch the function's core implementation; - a way to compose these decorated functions; (This definition is an oversimplification of the actual definition of a monad, but good enough for our purposes.) To understand what a monad is, I believe it is useful to explain what sort of problem monads solve. Suppose for instance that you wish for your functions to provide a log when they're run. If your function looks like this: ```{r} my_sqrt <- function(x){ sqrt(x) } ``` Then you would need to rewrite this function like this: ```{r} my_sqrt <- function(x, log = ""){ list(sqrt(x), c(log, paste0("Running sqrt with input ", x))) } ``` There are two problems with such an implementation: - we need to rewrite every function we need to use so that they provide logs; - these functions don't compose. What do I mean with "these functions don't compose"? Consider another such function `my_log()`: ```{r} my_log <- function(x, log = ""){ list(log(x), c(log, paste0("Running log with input ", x))) } ``` `sqrt()` and `log()` compose, or rather, they can be chained: ```{r} 10 |> sqrt() |> log() ``` while this is not true for `my_sqrt()` and `my_log()`: ```{r, eval = FALSE} 10 |> my_sqrt() |> my_log() ``` ``` Error in log(x) (from #3) : non-numeric argument to mathematical function ``` This is because `my_log()` expects a number, not a list which is what `my_sqrt()` returns. A "monad" is what we need to solve these two problems. The first problem, not having to rewrite every function, can be tackled using [function factories](https://adv-r.hadley.nz/function-factories.html). Let's write one for our problem: ```{r} log_it <- function(.f, ..., log = NULL){ fstring <- deparse(substitute(.f)) function(..., .log = log){ list(result = .f(...), log = c(.log, paste0("Running ", fstring, " with argument ", ...))) } } ``` We can now create our functions easily: ```{r} l_sqrt <- log_it(sqrt) l_sqrt(10) l_log <- log_it(log) l_log(10) ``` We can call `l_sqrt()` and `l_log()` *decorated* functions and the values they return *monadic* values. The second issue remains though; `l_sqrt()` and `l_log()` can't be composed/chained. To solve this issue, we need another function, called `bind()`: ```{r} bind <- function(.l, .f, ...){ .f(.l$result, ..., .log = .l$log) } ``` Using `bind()`, it is now possible to compose `l_sqrt()` and `l_log()`: ```{r} 10 |> l_sqrt() |> bind(l_log) ``` `bind()` takes care of providing the right arguments to the underlying function. We can check that the result is correct by comparing it the `$result` value from the returned object to `log(sqrt(10))`: ```{r} log(sqrt(10)) ``` This solution of using a function factory and defining a helper function to make the decorated functions compose is what constitutes a monad, but strictly speaking, this is not precisely correct. It can be interesting to see the actual definition from the programming language Haskell, which is a pure functional programming language where monads *must* be used to solve certain issues: *Monads can be viewed as a standard programming interface to various data or control structures, which is captured by Haskell's Monad class. All the common monads are members of it:*
class Monad m where
  (>>=)  :: m a -> (  a -> m b) -> m b
  (>>)   :: m a ->  m b         -> m b
  return ::   a                 -> m a
(Source: [Monad](https://wiki.haskell.org/Monad)) This definition is quite cryptic, especially if you don't know Haskell, but what this means is that a `Monad` (in Haskell) is *something* that has three methods: - `>>=` which is what we called `bind()`; - `>>` which I didn't bother implementing, because it's not really needed for understanding what a monad is; - and `return`. Don't be confused by the name, this has nothing to do with the `return()` we use inside functions to return a value. `return` is a function that wraps (or converts) a value into a monadic value, so if you consider any object `a`, `return` takes `a` as an input and *returns* the monadic value `m a`. While we didn't implement `return` (also called `unit`, which is also not a good name), our function factory `log_it()` does `return`/`unit`'s job but it *returns* `m f(a)` instead of `m a`. Using function factories comes more naturally to R users than using `return`/`unit`, hence why I did not focus on `return`/`unit`. Also, using our function factory, it is easy to implement `return/unit`: ```{r} unit <- log_it(identity) ``` so `return/unit` is just the `identity()` function that went through the function factory. In a sense, the function factory is even more necessary for defining a monad than `return/unit`. Finally, you might read sometimes that monads are objects that have a `flatmap()` method. I think that this definition as well is not strictly correct and very likely an oversimplification. But what is `flatmap()` anyways? In practical terms, it is equivalent to `bind()`, but it is how you get there that's different. To implement `flatmap()` two additional functions are needed: `fmap()` and `flatten()` (which is quite often called `join()`, but this has nothing to do with *joining* data frames, so I used `flatten()` instead). `fmap()` is a function that takes a monadic value as an argument and an undecorated function and applies this undecorated function to the monadic value: ```{r} fmap <- function(m, f, ...){ fstring <- deparse(substitute(f)) list(result = f(m$result, ...), log = c(m$log, paste0("fmapping ", fstring, " with arguments ", paste0(m$result, ..., collapse = ",")))) } ``` Let's first define a monadic value: ```{r} # Let’s use unit(), which we defined above, for this. (m <- unit(10)) ``` Let's now use `fmap()` to apply a non-decorated function to `m`: ```{r} fmap(m, log) ``` Great, now what about `flatten()` (or `join()`)? Why is that useful? Suppose that instead of `log()` we used `l_log()` with `fmap()` (so we’re using a decorated function instead of an undecorated one): ```{r} fmap(m, l_log) ``` As you can see from the output, this produced a nested list, a monadic value where the value is itself a monadic value. We would like `flatten()/join()` to take care of this for us. So this could be an implementation of `flatten()`: ```{r} flatten <- function(m){ list(result = m$result$result, log = c(m$log)) } ``` Let's try now: ```{r} flatten(fmap(m, l_log)) ``` Great! Now, as explained earlier, `flatmap()` and `bind()` are the same thing. But we have implemented `flatten()` and `fmap()`, so how do these two functions relate to `flatmap()`? It turns out that `flatmap()` is the composition of `flatten()` and `fmap()`: ```{r} # I first define a composition operator for functions `%.%` <- \(f,g)(function(...)(f(g(...)))) # I now compose flatten() and fmap() # flatten %.% fmap is read as "flatten after fmap" flatmap <- flatten %.% fmap ``` So this means that we can now replace: ```{r} 10 |> l_sqrt() |> bind(l_log) ``` by: ```{r} 10 |> l_sqrt() |> flatmap(l_log) ``` and we get the same result (well, not quite, since the log is different). I prefer introducing monads using `bind()`, because `bind()` comes as a natural solution to the problem of decorated functions not composing. Not so with `flatmap()`, but in some applications it might be easier to first define `flatten()` and `join()` and get `flatmap()` instead of trying to write `bind()` directly, so it’s good to know both approaches. Before continuing with the final part of this introduction, I just want to share with you that lists are also monads. We have everything we need: `as.list()` is `unit()`, `purrr::map()` is `fmap()` and `purrr::flatten()` is `flatten()`. This means we can obtain `flatmap()` from composing `purrr::flatten()` and `purrr::map()`: ```{r} # Since I'm using `{purrr}`, might as well use purrr::compose() instead of my own implementation flatmap_list <- purrr::compose(purrr::flatten, purrr::map) # Functions that return lists: they don't compose! # no worries, we implemented `flatmap_list()` list_sqrt <- \(x)(as.list(sqrt(x))) list_log <- \(x)(as.list(log(x))) 10 |> list_sqrt() |> flatmap_list(list_log) ``` (thanks to [@armcn_](https://twitter.com/armcn_/status/1511705262935011330?s=20&t=UfwIjsqyOX7-UbTMBHOCuw) for showing me this) In sum, monads are useful when you need values to also carry something more with them. This *something* can be a log, as shown here, but there are many examples. For another example of a monad implemented as an R package, see the [maybe monad](https://armcn.github.io/maybe/). `{chronicle}` actually takes advantage of the `{maybe}` package and uses the maybe monad to handle cases where functions fail. I provide a short introduction to the maybe monad in the [Maybe monad vignette](https://b-rodrigues.github.io/chronicler/articles/maybe-monad.html). # Monadic laws Monads need to satisfy the so-called "monadic laws". We're going to verify if the monad implemented in `{chronicler}` satisfies these monadic laws. ## First law The first law states that passing a monadic value to a monadic function using `bind()` (or in the case of the `{chronicler}` package `bind_record()`) or passing a value to a monadic function is the same. ```{r} a <- as_chronicle(10) r_sqrt <- record(sqrt) test_that("first monadic law", { expect_equal(bind_record(a, r_sqrt)$value, r_sqrt(10)$value) }) ``` Turns out that this is not quite the case here; the logs of the two objects will be slightly different. So I only check the value. ## Second law The second law states that binding a monadic value to `return()` (called `as_chronicle()` in this package, in other words, the function that coerces values to chronicler objects) does nothing. Here again we have an issue with the log, that's why I focus on the value: ```{r} test_that("second monadic law", { expect_equal(bind_record(a, as_chronicle)$value, a$value) }) ``` ## Third law The third law is about associativity; applying monadic functions successively or composing them first gives the same result. ```{r} a <- as_chronicle(10) r_sqrt <- record(sqrt) r_exp <- record(exp) r_mean <- record(mean) test_that("third monadic law", { expect_equal( ( (bind_record(a, r_sqrt)) |> bind_record(r_exp) )$value, ( a |> (\(x) bind_record(x, r_sqrt) |> bind_record(r_exp))() )$value ) }) ``` # flatmap() for `chronicle` objects For exhaustivity's sake, I check that I can get `flatmap_record()` by composing `flatten_record()` and `fmap_record()`: ```{r} r_sqrt <- record(sqrt) r_exp <- record(exp) r_mean <- record(mean) a <- 1:10 |> r_sqrt() |> bind_record(r_exp) |> bind_record(r_mean) flatmap_record <- purrr::compose(flatten_record, fmap_record) b <- 1:10 |> r_sqrt() |> flatmap_record(r_exp) |> flatmap_record(r_mean) identical(a$value, b$value) ```