<

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.

fmapviews::transform
pureviews::single, yield
apply*views::zip_with
bindviews::for_each

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:
tomato2.4
mozzarella2.7
basil3.2

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.





Applicative:
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