Getting started with
the Reader Monad

Ruzena, www.walletfox.com

15.12.2021, Italian C++

Learn about the Reader Monad

Monads are functional design patterns

They solve a large range of problems that come out in functional programming

Composition with side effects requires a common interface

For Monads this means two higher order functions: pure and bind

Let's relate this to something you might know: range-v3

Pure lifts values into other domains

Applicative views::single, as well as monadic yield are world-crossing operations. They lift a value into a range.

Transforming and joining is the task for bind

Bind in range-v3 is called views::for_each

views::for_each is the most unfortunate name for a transform / flatten operation.

These are the higher order functions that we will need today

You can think of range-v3 for reference.

pureviews::single, yield

Monads are also Functors and Applicatives

We often need to use functor and applicative aspects rather than the monadic aspect

The particularity of a Monad is that it allows for a choice, i.e. different paths in a computation

The Reader Monad propagates shared environment untouched along the computational chain

The environment is often a database, a repository or a configuration

The environment is not readily available and will be injected later

The Reader Monad removes an explicit use of the dependency from the computational chain's API

The dependency is moved from the input arguments into the return type

The return type is a now a function <A(E)>.

In principle the Reader is a function

This is how Reader compares to std::vector for fmap:

Let's look at a concrete example

A shop will run an offer for a bundle of ingredients to make Caprese

The prices of ingredients will be retrieved
from a database

The database:

We will perform operations on the data retrieved from the yet unavailable database

We wish to remove any explicit mention of the database dependency from the call chain

Remember that we are not allowed to hold any state.

Since the database isn't available, the Reader must be curried

We can deliver the key but not the map.

Replacing m with _ clarifies that we are missing the database

This is a common starting point for monadic transformations.

Prices independent from the database follow the same structure but ignore the environment.

The independent price is being lifted into the Reader environment.

read_price and just_price instances are the subjects of our transformations

They can be constructed using fmap, ask and a selector function.
We skip this.

Think of monads as transformations. Don't dwell too much on monad constructors.

Remember that read_price('t') doesn't return a value, it'a function of the environment.

We obtain the actual value much later once we inject the database

This is somewhat like stubbing in testing.

Our Caprese example demonstrates
three facets of the Reader

Functor: Let's apply discount to the tomato

We will get the tomato price from the database that isn't available. The input is the 'stub' reader read_price('t')

Discount is a pure function
(double → double)

This is the task for fmap

The actual values are produced only once we inject the database

fmap executes the action ra in the environment e and constructs a new action rb

ra(e) gives us a value (price etc.) after we inject the database

This happens when we use the call operator of the composed lambda.

Let's add prices of two items together

This is the job of pure & apply

Addition is a binary function but apply takes a unary function.
How do we deal with this?

We provide a curried add function that can take one argument at a time

We lift the curried add function
into the Reader world

We use apply in a nested fashion

Things get out of hand pretty fast

We need a general curry function

Stackoverflow → CTRL+C → CTRL+V

Currying in C++

Credit: Julian

Now with better curry

pure lifts a value into the Reader world

a...in our case the curried add function

How come we did not lift the discount function in case of fmap?

This is because fmap did it for us

apply constructs a follow-up action by running both the lifted function and ra in the environment

The position of apply determines
what rf(e)(ra(e)) represents

positionra(e)rf(e) (ra(e))eventually
outer apply0.5f1 (0.5)-0.5
middle apply2.7f2 (2.7) (?)0.9*2.7 - 0.5
inner apply2.4f3 (2.4) (?) (?)2.4 + 0.9*2.7 - 0.5

Applicatives let us chain operations

But only monads allow us to select the next computation based on the result of the previous computation

Monad: Let's choose the next item based on the price of the previous item

The monadic aspect is implemented using pure (return) & bind

fmap and apply
can be expressed in terms of
pure and bind

But this approach might introduce some unnecessary sequencing

Let's pick a cheaper tomato if the basil & mozzarella are too expensive

The function that picks tomatoes is
a world-crossing function

Hint: Returning std::optional<int> when parsing a string is also a world-crossing function.

bind ties a world-crossing function to a monad

  1. pick_tomato expects a boolean but read_1 is a monad
  2. also, based on the bool we need to construct read_price('x') or read_price('t')

We had to pick the cheaper tomato

Due to the presence of the Reader in the world-crossing function bind constructs an intermediate action

bind constructs an intermediate action

ra(e)determine if price was exceeded... true / false
f(ra(e))based on the result construct read_price('x') or read_price('t')
f(ra(e))(e)execute read_price('x') or read_price('t')

Finally, here are the most important takeaways