Grokking the Reader Monad

choc13

Matt Thornton

Posted on May 1, 2021

Grokking the Reader Monad

From its name the reader monad doesn’t give too many clues about where it would be useful. In this post we’ll grok it by inventing it ourselves in order to solve a real software problem. From this we’ll see that it’s actually one way of doing dependency injection in functional programming.

Prerequisites

There won’t really be any theory here, but it’ll be easier if you’ve already grokked monads. If you haven’t then check out Grokking Monads from earlier in this series and then head back over here.

The scenario

Let’s imagine we’ve been asked to write some code that charges a user’s credit card. To do this we’re going to need to lookup some information from a database and also call a payment provider.

Our domain model will look like this.

type CreditCard =
   { Number: string
     Expiry: string
     Cvv: string }

type EmailAddress = EmailAddress of string 
type UserId = UserId of string

type User =
    { Id: UserId
      CreditCard: CreditCard
      EmailAddress: EmailAddress }
Enter fullscreen mode Exit fullscreen mode

We'll also start with a Database module containing a function that can read a User and a PaymentProvider module that contains a function that can charge a CreditCard. They look something like this.

type ISqlConnection =
    abstract Query : string -> 'T

module Database =

    let getUser (UserId id) : User =
        let connection = SqlConnection("my-connection-string")
        connection.Query($"SELECT * FROM User AS u WHERE u.Id = {id}")

type IPaymentClient =
    abstract Charge : CreditCard -> float -> PaymentId

module PaymentProvider =
    let chargeCard (card: CreditCard) amount = 
        let client = new PaymentClient("my-payment-api-secret")
        client.Charge card amount
Enter fullscreen mode Exit fullscreen mode

Our first implementation

Let’s start off with the easiest solution we can think of. We’ll call the database to lookup the user, get the credit card from their profile and call the payment provider to charge it.

let chargeUser userId amount =
    let user = Database.getUser userId
    PaymentProvider.chargeCard user.CreditCard amount
Enter fullscreen mode Exit fullscreen mode

Super easy, given that we already had Database.getUser and PaymentProvider.chargeCard ready to use.

The amount of coupling here is probably making you feel a bit queasy though. Invoking getUser and chargeCard functions directly isn't itself a problem. The problem really lies further down with how those functions themselves are implemented. In both cases they're instantiating new clients like SqlConnection and PaymentClient which creates a few problems:

  1. Hard coded connection strings mean we're stuck talking to the same database instance in all environments.
  2. Connection strings usually contain secrets which are now checked into source control.
  3. Writing unit tests isn't possible because it's going to be calling the production database and payment provider. I suppose that's one way to foot the CI bill when running all of those unit tests 😜

Inversion of Control 🔄

You’re probably not surprised to learn that the solution to this is to invert those dependencies. Inversion of Control (IoC) transcends paradigms, it’s a useful technique in both OOP and FP. It’s just that whereas OOP tends to utilise constructor injection via reflection in FP we'll see there are other solutions available to us.

What’s the easiest IoC technique for a function then? Just pass those dependencies in as parameters. It's like OOP class dependencies, but at the function level.

module Database =
    let getUser (connection: ISqlConnection) (UserId id) : User =
        connection.Query($"SELECT * FROM User AS u WHERE u.Id = {id}")

module PaymentProvider =
    let chargeCard (client: IPaymentClient) (card: CreditCard) amount = 
        client.Charge card amount

let chargeUser sqlConnection paymentClient userId amount =
    let user = Database.getUser sqlConnection userId
    PaymentProvider.chargeCard paymentClient user.CreditCard amount
Enter fullscreen mode Exit fullscreen mode

No surprises there. We’ve just supplied the necessary clients as parameters and passed them along to the function calls that need them. This solution does have it downsides though. The primary one being that as the number of dependencies grows the number of function parameters can become unruly.

On top of this most applications have some degree of layering to them. As we introduce more layers, to break down and isolate the responsibilities of individual functions, we start needing to pass some dependencies down through many layers. This is typical of any IoC solution, once you flip those dependencies it cascades right through all the layers of your application. It’s turtles inverted dependencies all the way down.

A partial solution

What we’d like to avoid is having to explicitly pass those transitive dependencies into functions like chargeUser where they’re not being used directly. On the other hand we don’t want to lose compile time checking by falling back to reflection based dependency injection.

What if we moved those dependency parameters to the end of the function signature? That way we can use partial application to defer supplying them until the last minute, when we're ready to "wire up the application". Let's try with those service modules first.

module Database =
    let getUser (UserId id) (connection: ISqlConnection) : User =
        connection.Query($"SELECT * FROM User AS u  WHERE u.Id = {id}")

module PaymentProvider =
    let chargeCard (card: CreditCard) amount (client: IPaymentClient) = 
        client.Charge card amount
Enter fullscreen mode Exit fullscreen mode

With that we can create a new function that gets the user when passed a connection by simply writing the following.

let userFromConnection: (ISqlConnection -> User) = Database.getUser userId
Enter fullscreen mode Exit fullscreen mode

And we can do a similar thing when charging the card.

let chargeCard: (IPaymentClient -> PaymentId) = PaymentProvider.chargeCard card amount
Enter fullscreen mode Exit fullscreen mode

Alright, let's stick it together and re-write our chargeUser function.

let chargeUser userId amount =
    let user = Database.getUser userId
    // Problem, we haven’t got the user now, but a function that needs a ISqlConnection to get it
    PaymentProvider.chargeCard user.CreditCard amount
    // So the last line can’t access the CreditCard property 
Enter fullscreen mode Exit fullscreen mode

It's good, but it's not quite right! We've eliminated the two dependency parameters from the chargeUser function, but it won't compile. As the comment points out we don’t have a User like we need to, but rather a function that has the type ISqlConnection -> User. That's because we've only partially applied Database.getUser and to finish that call off and actually resolve a User, we still need to supply it with a ISqlConnection.

Does that mean we're going to need to pass in the ISqlConnection to chargeUser again? Well if we could find a way to lift up PaymentProvider.chargeCard so that it could work with ISqlConnection -> User instead of just User then we could get it to compile. In order to do this we need to create a new function that takes a ISqlConnection as well as the function to create a User given a ISqlConnection and the amount we want to charge the user.

We don't really have a good name for this function because outside of this context it doesn't really make sense to have a chargeCard function that depends on a ISqlConnection. So what we can do instead is create an anonymous function, a lambda, inside of chargeUser that does this lifting for us.

let chargeUser userId amount: (ISqlConnection -> IPaymentClient -> PaymentId) =
    let userFromConnection = Database.getUser userId

    fun connection ->
        let user = userFromConnection connection
        PaymentProvider.chargeCard user.CreditCard amount
Enter fullscreen mode Exit fullscreen mode

I've annotated the return type of chargeUser to highlight the fact that it's now returning a new function, that when supplied with both dependencies of ISqlConnection and IPaymentClient, will charge the user.

At this point, we've managed to defer the application of any dependencies, but the solution is a bit cumbersome still. If, at a later date, we need to do more computations in chargeUser that require yet more dependencies, then we're going to be faced with even more lambda writing. For instance imagine we wanted to email the user a receipt with the PaymentId. Then we'd have to write something like this.

let chargeUser userId amount =
    let userFromConnection = Database.getUser userId

    fun connection ->
        let user = userFromConnection connection
        let paymentIdFromClient = PaymentProvider.chargeCard user.CreditCard amount

        fun paymentClient ->
            let (PaymentId paymentId) = paymentIdFromClient paymentClient
            let email = EmailBody $"Your payment id is {paymentId}"
            Email.sendMail user.EmailAddress email
Enter fullscreen mode Exit fullscreen mode

😱

The nesting is getting out of hand, the code is becoming tiring to write and the number of dependencies we eventually need to supply to this function is getting unwieldy too. We're in a bit of a bind here.

Binding our way out of a bind

Let's see if we can write a function called injectSqlConnection that will allow us to simplify chargeUser by removing the need for us to write the lambda that supplies the ISqlConnection. The goal of this is to allow us to write chargeUser like this.

let chargeUser userId amount =
    Database.getUser userId
    |> injectSqlConnection (fun user -> PaymentProvider.chargeCard user.CreditCard amount)
Enter fullscreen mode Exit fullscreen mode

So injectSqlConnection needs to take a function that requires a User as the first parameter and a function that can create a User given a ISqlConnection as the second parameter. Let's implement it.

let injectSqlConnection f valueFromConnection =
    fun connection ->
        let value = valueFromConnection connection
        f value
Enter fullscreen mode Exit fullscreen mode

In fact, that function doesn't depend on the ISqlConnection in anyway. It works for any function f that needs a value a which can be created when passed some dependency. So let's just call it inject from now on to acknowledge that it works for any type of dependency.

You just discovered the reader monad 🤓

The inject function is letting us sequence computations that each depend on a wrapped value returned from the last computation. In this case the value is wrapped in a function that requires a dependency. That pattern should look familiar because we discovered it when Grokking Monads. It turns out that we've in fact discovered bind again, but this time for a new monad.

This new monad is normally called Reader because it can be thought of as reading some value from an environment. In our case we could call it DependencyInjector because it's applying some dependency to a function in order to return the value we want. The way to bridge the mental gap here it to just think of dependency injection as a way to read a value from some environment that contains the dependencies.

A little lie 🤥

Actually, the implementation of inject above isn't quite right. If we rewrite the more complex chargeUser, the one that also sends an email, using inject Then we’ll see how it breaks.

let chargeUser userId amount =
    Database.getUser userId
    |> inject (fun user -> PaymentProvider.chargeCard user.CreditCard amount)
    |> inject (fun (PaymentId paymentId) -> 
        let email =
            EmailBody $"Your payment id is {paymentId}"

        let address = EmailAddress "a.customer@example.com"
        Email.sendMail address email)
Enter fullscreen mode Exit fullscreen mode

This actually fails on the second inject. That's because after the first call to inject it returns the following type ISqlConnection -> IPaymentClient -> PaymentId. Now on the second call to inject we have two dependencies to deal with, but our inject function has only been designed to supply one, so it all falls down.

The solution to this is to create a single type that can represent all of the dependencies. Basically we want the chargeUser function to have the signature UserId -> float -> Dependencies -> TransactionId rather than UserId -> float -> ISqlConnection -> IPaymentClient -> TransactionId. If we can do that then we just need to make one small adjustment to inject to make things work again.

let inject f valueThatNeedsDep =
    fun deps ->
        let value = valueThatNeedsDep deps
        f value deps
Enter fullscreen mode Exit fullscreen mode

Notice how this time we also supply deps to f on the final line? It's subtle but it changes the return type of inject to be ('deps -> 'c), where 'deps is the type of dependencies also required by valueThatNeedsDep.

So what's happened here is that we've now constrained the output of inject to be a new function that requires the same type of 'deps as the original function. That's important because it means our dependencies are now unified to a single type and we can happily keep chaining computations that require those dependencies together.

Uniting dependencies 👭

There are several ways to unite all of the dependencies together in a single type, such as explicitly creating a type with fields to represent each one. One of the neatest with F# though is to use inferred inheritance. Inferred inheritance means we let the compiler infer a type that implements all of the dependency interfaces we require.

In order to use inferred inheritance we need to add a # to the front of the type annotations for each dependency. Let's make that change in the Database and PaymentProvider modules to see what that looks like.

module Database =
    let getUser (UserId id) (connection: #ISqlConnection) : User =
    // Implementation of getUser

module PaymentProvider =
    let chargeCard (card: CreditCard) amount (client: #IPaymentClient): TransactionId =
    // Implementation of chargeCard
Enter fullscreen mode Exit fullscreen mode

All we've changed is to write #ISqlConnection instead of ISqlConnection and #IPaymentClient instead of IPaymentClient. From this F# can union these types together for us when it encounters something that needs to satisfy both constraints. Then at the root of the application we just have to create an object that implements both interfaces in order to satisfy the constraint.

The upshot of this is that F# infers the type signature of chargeUser to be UserId -> float -> ('deps -> unit) and it requires that 'deps inherit from both ISqlConnection and IPaymentProvider.

A final improvement

We've pretty much reached our stated goal now of eliminating all of the explicit dependency passing between functions. However, I think it's still a bit annoying that we have to keep creating lambdas to access the values like user and paymentId when calling inject to compose the operations. We've seen before, in Grokking Monads, Imperatively, that it's possible to write monadic code in an imperative style by using computation expressions.

All we have to do is create the computation expression builder using the inject function we wrote earlier, as that's our monadic bind. We'll call this computation expression injector because that's more relevant to our use case here, but typically it would be called reader.

type InjectorBuilder() =
    member _.Return(x) = fun _ -> x
    member _.Bind(x, f) = inject f x
    member _.Zero() = fun _ -> ()
    member _.ReturnFrom x = x

let injector = InjectorBuilder()

let chargeUser userId amount =
    injector {
        let! user = Database.getUser userId
        let! paymentId = PaymentProvider.chargeCard user.CreditCard amount
        let email =
            EmailBody $"Your payment id is {paymentId}"

        return! Email.sendMail user.Email email
    }
Enter fullscreen mode Exit fullscreen mode

😍
By simply wrapping the implementation in injector { } we’re basically back to our very first naïve implementation, except this time all of the control is properly inverted. Whilst the transitive dependencies are nicely hidden from sight they’re still being type checked. In fact if we added more operations to this later that required new dependencies then F# would automatically add them to the list of required interfaces that must be implemented for the 'deps type in order to finally invoke this function.

When we're finally ready to call this function, say at the application root where we have all of the config to hand in order to create the dependencies, then we can do it like this.

type IDeps =
    inherit IPaymentClient
    inherit ISqlConnection
    inherit IEmailClient

let deps =
    { new IDeps with
        member _.Charge card amount =
            // create PaymentClient and call it

        member _.SendMail address body =
            // create SMTP client and call it

        member _.Query x =
            // create sql connection and invoke it 
            }

let paymentId = chargeUser (UserId "1") 2.50 deps
Enter fullscreen mode Exit fullscreen mode

Where we use an object expression to implement our new IDeps interface on the fly so that it satisfies all of the inferred types required by chargeUser.

Quick recap 🧑‍🎓

We started off by trying to achieve inversion of control to remove hardcoded dependencies and config. We saw that doing this naïvely can lead to an explosion in the number of function parameters and that it can cascade right through the application. In order to solve this we started out with partial application to defer supplying those parameters until we were at the application root where we had the necessary config to hand. However, this solution meant that we couldn't easily compose functions that required dependencies and it was even more tricky when they required different types of dependencies.

So we invented an inject function that took care of this plumbing for us and realised that we'd actually discovered a new version of bind and hence a new type of monad. This new monad is commonly known as Reader and it's useful when you need to compose several functions that all require values (or dependencies) that can be supplied by some common environment type.

If you want to use the reader monad in practice then you can find an implementation that's ready to roll in the FSharpPlus library.

Appendix

The format of the reader monad is often a little different in practice to how it was presented here. Expand the section below if you want more details.

Usually when implementing the reader monad we create a new type to signify it, called Reader, in order to distinguish it from a regular function type. I left it out above because it's not an important detail when it comes to grokking the concept, but if you're looking to use the technique then you'll likely encounter it in this wrapped form. It's a trivial change and the code would just look like this instead.

type Reader<'env, 'a> = Reader of ('env -> 'a)

module Reader =
    let run (Reader x) = x
    let map f reader = Reader((run reader) >> f)

    let bind f reader =
        Reader
            (fun env ->
                let a = run reader env
                let newReader = f a
                run newReader env)

    let ask = Reader id

type ReaderBuilder() =
    member _.Return(x) = Reader (fun _ -> x)
    member _.Bind(x, f) = Reader.bind f x
    member _.Zero() = Reader (fun _ -> ())
    member _.ReturnFrom x = x

let reader = ReaderBuilder()

let chargeUser userId amount = 
    reader {
        let! (sqlConnection: #ISqlConnection) = Reader.ask
        let! (paymentClient: #IPaymentClient) = Reader.ask
        let! (emailClient: #IEmailClient) = Reader.ask
        let user = Database.getUser userId sqlConnection
        let paymentId = PaymentProvider.chargeCard user.CreditCard amount paymentClient

        let email =
            EmailBody $"Your payment id is {paymentId}"

        return Email.sendMail user.Email email emailClient
    }
Enter fullscreen mode Exit fullscreen mode

The return type of chargeUser is now Reader<'deps, unit> where 'deps will have to satisfy all of those interfaces marked with # as before.

I've also had to use Reader.ask to actually get dependencies out of the environment in this case. The reason for this is because functions like Database.getUser do not return a Reader in their current form. We could create a Reader on the fly by doing Reader (Database.getUser userId) but sometimes that can also be cumbersome, especially if we're working with client classes rather than functions, which is often the case. So having ask in our toolkit can be a nice way to just get hold of the dependency and use it explicitly in the current scope.


💖 💪 🙅 🚩
choc13
Matt Thornton

Posted on May 1, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Grokking Lenses
fsharp Grokking Lenses

May 28, 2021

Interpreting Free Monads
fsharp Interpreting Free Monads

May 21, 2021

Grokking Free Monads
fsharp Grokking Free Monads

May 14, 2021

Grokking Monad Transformers
fsharp Grokking Monad Transformers

May 7, 2021

Grokking the Reader Monad
fsharp Grokking the Reader Monad

May 1, 2021