State monad
How the state monad can be used to separate concerns
Published 2019-10-12 by Olivia MackintoshThis is a short stream-of-thought work post (originally a Slack message) I made some time ago whilst trying to learn and understand category theory.
Maybe this should be in #random as it’s a random thought but it’s also kind of engineering related. I did some further FP reading over the past few weeks about Kleisli categories and extra composition tidbits. They’re literally amazing and totally make sense for chaining together a bunch of transformations/actions but also abstracting things like logging/audits/messages to maintain separation of concerns.
One common pattern is to do something like the following when logging - this happens probably in 90% of business apps:
= ""
log
def add_four(number):
+= "Added four\n"
log return number + 4
Some issues with this approach:
- Why does a function called
add_four
know how to log stuff? - Global state - what about multithreaded apps (locking not nice)?
It also gets hairier when you have a series of actions chained together as you have more complexity.
Suppose you have the following two functions:
def add_four(number):
return (number + 4, "Added four\n")
def double(number):
return (number * 2, "Doubled\n")
Now, these functions are pure, there’s no global state and the functions don’t know anything about how to log only what to log as a piece of metadata in the result. Standard functional composition might look like this:
def compose2(f, g):
return lambda x: f(g(x))
Suppose we wanted to compose a function that adds four to a number and then double it. We might do this: compose2(double, add_four)
. The problem is that the implicit types / arity doesn’t add up: you can’t pass a tuple to a function that takes only a number. If we had a modified compose function we can compose double and add_four to give us a new function that will result as below:
= h(2)
result print(result)
> (12, "Added four\n Doubled\n")
You can keep chaining functions ad-infinitum as long as the first argument of our result tuple lines up with the input of the next function. It’s a bit awkward to do in Python and requires some a really weird looking lift function but this is actually the essence of a monad. I know lots of people toss the word around and show gnarly looking type signatures and Haskell code but it’s actually quite simple when you get it. If people find it interesting I might do a talk.