A Small (Real) Example of the Reader and Writer Monads
Posted
27Jul2014
by
Channing Walton
This is an example of using the Reader and Writer monads to solve a problem which cropped up on a project I am working on.
Before getting to the problem, what are the Reader and Writer monads?
A Reader, sometimes called the environment monad, treats functions as values in a context (see LYAH). Loosely speaking, it allows you to build a computation that is a function of some context (configuration, session, database connection, etc.), rather than passing the context as an argument to the function.
A Writer is a monad that attaches a log or some other accumulated data to a value.
(Note that the code below is also available on
Github.)
The Problem
The problem arose working with a database in a web app. Obviously the following are desirable:
Operations should run in a transaction/connection/context
Transactions should be rolled back in the event of a failure
Post commits should be supported
A Problematic Start
Wow. Side-effect-tastic. But typical.
The first problem is that devs must remember to use ‘run’ to get transactions as there is no compile time enforcement. In our case the framework
we were using would just magic one up, who knows what was going on.
Another problem is the lack of an explicit declaration of the context code is running in so that devs have no idea whether code is running database
work or not. And since the only error management is exceptions, code becomes very guarded and messy.
So, nested calls to ‘run’, no calls to ‘run’, no way to know if functions make database calls so layers of abstraction above the database look like
simple functions, all contribute to a confused state of affairs.
Lets solve the transaction problem first.
Introducing the Reader
Note that to compile the following code you need Scalaz 7.0.6.
Observations
Everything is now in for-comprehensions rather than the usual imperative style.
The value returned from the for-comprehension is a Work[A], so nothing happens until that Work is run in Database.run.
Importantly, it is no longer possible to operate on the Database outside of a Transaction.
Any functions building on the Database will return Work[A] thus making it very obvious what the context of those functions are.
In the project that this example comes from, this alone revealed a number of of sins which were resolved resulting in clearer
code.
What about post-commits and errors?
We will solve the post commits issue using a Writer that accumulates post commits - functions run when the transaction succeeds. But,
to avoid wrapping the Reader in a Writer, and getting nested for-comprehensions as a result,
a monad transformer will be used to combine the Reader with the Writer. Fortunately,
scalaz provides a ReaderWriterState monad which will suffice if we ignore the State, setting it to Unit.
Errors will be handled by scalaz’s answer to scala’s Either, \/[Throwable, A], with the left being an exception and the right being the result.
Now its impossible to run code outside a transaction, post commits are easily added, errors are returned nicely and not thrown.
Furthermore, operations are easy to test since they return values which can be checked easily, rather than side-effects which must be captured.
We encourage discussion of our blog posts on our Gitter channel.
Please review our Community Guidelines before posting there.
We encourage discussion in good faith, but do not allow combative, exclusionary, or harassing behaviour.
If you have any questions, contact us!