This content originally appeared on DEV Community and was authored by Matt Thornton
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: EmailAdress }
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
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
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:
- Hard coded connection strings mean we're stuck talking to the same database instance in all environments.
- Connection strings usually contain secrets which are now checked into source control.
- 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
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
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
And we can do a similar thing when charging the card.
let chargeCard: (IPaymentClient -> PaymentId) = PaymentProvider.chargeCard card amount
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
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
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
?
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)
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
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)
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
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 =
module PaymentProvider =
let chargeCard (card: CreditCard) amount (client: #IPaymentClient): TransactionId
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
}
?
By simply wrapping the implementation in reader { }
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
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
}
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.
This content originally appeared on DEV Community and was authored by Matt Thornton
Matt Thornton | Sciencx (2021-05-01T08:10:34+00:00) Grokking the Reader Monad. Retrieved from https://www.scien.cx/2021/05/01/grokking-the-reader-monad/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.