## DEV Community

Matt Thornton

Posted on • Updated on

# Grokking Monads, Imperatively

Previously, in Grokking Monads, we discovered monads by going through a worked example. By the end we'd created the basic machinery in the form of a andThen function for the option type, but we hadn't quite reached our ultimate goal. We were hoping to write the code in the same way that we would if we didn't have to deal with option values. We wanted to write it in a more "imperative" style. In this post we're going to see how to achieve that with F#'s computation expressions whilst also deepening our intuition about monads.

# Recap

Let's quickly recap the domain model

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

type User =
{ Id: UserId
CreditCard: CreditCard option }

We wanted to write chargeUserCard with the signature

UserId -> TransactionId option

If we didn't have to deal with the option values then we could write it in the "imperative" style.

let chargeUserCardSafe (userId: UserId): TransactionId =
let user = lookupUser userId
let creditCard = u.CreditCard
chargeCard creditCard

Our goal was to write something that looked like this even when we had to deal with optional values in the middle of the computation.

We got as far as factoring out the following andThen function

let andThen f x =
match x with
| Some y -> y |> f
| None -> None

Using that the best we could manage was the following version of chargeUserCard

let chargeUserCard (amount: double) (userId: UserId): TransactionId option =
userId
|> lookupUser
|> andThen getCreditCard
|> andThen (chargeCard amount)

# What's the problem π€·ββοΈ

You might rightly be wondering what the issue is here. Our final implementation of chargeUserCard is readable. It quite clearly describes our intent and we've eliminated the repetitive code. So is this just a case of aesthetics?

To see why the ability to use the "imperative" style extends beyond simple aesthetics, let's introduce another requirement. This will stress our current implementation and show its weakness.

Users must now set spend limits on their profile. For backwards compatibility the limit is modelled as an option. If a limit exists we must check that the spend is under the limit, if the user has not yet set a limit we terminate the computation and return None.

This limit is stored in the User model like this.

type User =
{ Id: UserId
CreditCard: CreditCard option
Limit: double option }

Let's start by implementing a getLimit function (like we did for getCreditCard) along these lines.

let getLimit (user: User): double option =
user.Limit

So how do we go about updating chargeUserCard to take into account spend limits? We need to perform the following steps:

1. Lookup the user based on their id
2. If the user exists lookup the credit card
3. If the user exists lookup the limit
4. If the limit and credit card exist then charge the card providing the amount is less than the limit

Let's first write this as if there were no option values to give us something to aim for in the "imperative" style.

let chargeUserCardSafe (amount: double) (userId: UserId) =
let user = lookupUser userId
let card = getCreditCard user
let limit = getLimit user
if amount <= limit then
chargeCard amount card
else
None

Let's reintroduce the option values and naively convert it to the pipeline form using andThen.

let chargeUserCard (amount: double) (userId: UserId): TransactionId option =
userId
|> lookupUser
|> andThen getCreditCard
|> andThen getLimit
|> andThen
(fun limit ->
if amount <= limit then
chargeCard amount ??
else
None)

This won't compile for two reasons:

1. We can't write andThen getLimit after getCreditCard, because at that point we've got access to the CreditCard, but we need to pass a User into getLimit.
2. We don't have access to a CreditCard value at the point where we want to call chargeCard.

# Breaking the chain π

There no longer seems to be one sequential flow of data. This is because we need to use the user to lookup both the CreditCard and the limit and we need both of those things to exist before we can charge the card.

After some head scratching we can find a way to write this function in the pipeline style using andThen, but beware, it gets hairy!

let chargeUserCard (amount: double) (userId: UserId) : TransactionId option =
let applyDiscount discount = amount * (1. - discount)

userId
|> lookupUser
|> andThen
(fun user ->
user
|> getCreditCard
|> andThen
(fun cc ->
user
|> getLimit
|> Option.map (fun limit -> {| Card = cc; Limit = limit |})))
|> andThen
(fun details ->
if amount <= details.Limit then
chargeCard amount details.Card
else
None)

Wow! That escalated quickly!

Don't worry if you've not fully comprehended this implementation. That's kind of the point, it's become difficult to understand and we need to find a way to tame it.

When this type of scenario occurs it makes chaining cumbersome. In order to keep using the |> operator we need to accumulate more and more state so that we can finally use all of it at the end of the chain. This is what we used the anonymous record for above in the deeply nested part of the expression.

We might start to wonder whether functional programming is really so great. In the good old imperative days we'd just assign those values to a variable and then reference them all at the end of the method when we needed them.

# Having our cake and eating it π°

Fortunately there is a way out of this mess.

This time we're going to invent some new syntax to make it work with option values. We're going to define let!. It's like let but rather than just binding the name to the expression, it's going to bind the name to the value inside the option if it exists. If the value doesn't exist then it's going to terminate the function immediately with a value of None. This is exactly the same behaviour as the andThen function we invented before, except now it allows us to name the result.

With this new syntax chargeUserCard is simply

let chargeUserCard (amount: double) (userId: UserId) =
let! user = lookupUser userId
let! card = getCreditCard user
let! limit = getLimit user
if amount <= limit then
chargeCard amount card
else
None

Barely any difference to the version without the option. "That's great", I hear you say, "but you can't just invent new syntax!". Well lucky for us we don't have to. F# provides let! out of the box as part of a feature called Computation Expressions.

# Computation Expressions != Magic πͺ

F# isn't entirely magic though, we have to teach it how let! should behave for a given monad. We have to define a new computation expression.

I'm not going to go into great details about how to do this here, the F# docs are a good place to start for that. All that's relevant for us is that F# requires us to create a type with a Bind method. We already know how to write Bind because we discovered it and called it andThen. The computation expression builder for an option ends up looking like this.

let andThen f x =
match x with
| Some y -> y |> f
| None -> None

type OptionBuilder() =
member _.Bind(x, f) = andThen f x
member _.Return(x) = Some x
member _.ReturnFrom(x) = x

We also needed to define Return, which lets us return a regular value from the computation expression by wrapping it in a Some case and ReturnFrom, which lets us return the result of an expression which produces an option.

ReturnFrom might seem superfluous because it's so simple. However, in other computation expressions we might require more complex behaviours. By making it extensible F# has granted us that power at the expense of some trivial boilerplate in this case.

With the computation expression in place our final implementation of chargeUserCard becomes

let chargeUserCard (amount: double) (userId: UserId) =
option {
let! user = lookupUser userId
let! card = getCreditCard user
let! limit = getLimit user

return!
if amount <= limit then
chargeCard amount card
else
None
}

Pretty neat! We just need to wrap the body in option {} to indicate we wanted to use the option computation expression we just defined. We also had to use return! on the final line to tell it to return the option value produced by that expression.

# Test driving the computation expression π

To give some insights into the computation expression we just defined and to prove it really does behave as we want, let's run a few tests in the F# repl.

> option {
-     let! x = None
-     let! y = None
-     return x + y
- };;
val it : int option = None

So when both x and y are None then result is None. What about when just x or y are None?

> option {
-     let! x = Some 1
-     let! y = None
-     return x + y
- };;
val it : int option = None

> option {
-     let! x = None
-     let! y = Some 2
-     return x + y
- };;
val it : int option = None

3 for 3! We just need to make sure it actually returns a Some containing the addition when both x and y have some value.

> option {
-     let! x = Some 1
-     let! y = Some 2
-     return x + y
- };;
val it : int option = Some 3

Full marks! π

# More monad intuition

This "imperative" style might look familiar to you. If this were an async computation then let! is just like await. The reason people love async/await, especially those who remember the days of nested promise callback hell, is because it allows us to write programs as if they weren't async. It removes all of the noise associated with having to deal with the fact that results wont be immediately available and might also fail.

F#'s computation expressions allow us to make this work with all the monads flavours, not just async. This is really powerful as we can now write the code in an easy to comprehend "imperative" style, but without the mutable state and other side effects of fully imperative programming.

# Do I have to roll my own π

The F# core library includes some built in computation expressions for sequences, async workflows and LINQ query expressions. There are many more useful ones implemented in open source libraries too. FSharpPlus has even taken it a step up by creating a single monad computation expression which works for many monadic types.

# What did we learn π§βπ

We've seen that whilst the andThen function is the underlying machinery for chaining monadic computations, it can quickly become cumbersome to work with it directly when we don't have such an obvious linear sequence for the operations we want to perform. By utilising F#'s computation expressions we can hide this "plumbing" away and instead write the code as if we weren't dealing with a monad. This is exactly what async/await does, but just in the narrower sense of Tasks or Promises. So if you've grokked async/await then you're well on your way to having grokked monads and computation expressions.

## Latest comments (2)

Michael

Is there OptionBuilder CE in standart library? If not, why ?

Matt Thornton

There isnβt one in the standard library. I believe one of the main reasons for this is because there are different ways to implement it, such as strict vs lazy. This SO answer provides a good explanation.