The State Monad
You might be interested in exploring the State monad. I'll go through a couple parts of the program first and then include a complete runnable example at the bottom.
First, we'll cover the State monad. There are countless monad introductions online which are out of scope for this post, so I'm just going to cover just enough to get you up and running. Other implementations of State monad will have additional conveniences; be sure to learn about those if you're interested in learning more.
// State monad
const State = runState => ({
runState,
bind: f => State(s => {
let { value, state } = runState(s)
return f(value).runState(state)
}),
evalState: s =>
runState(s).value,
execState: s =>
runState(s).state
})
State.pure = y =>
State(x => ({ value: y, state: x }))
State.get = () =>
State(x => ({ value: x, state: x }))
State.put = x =>
State($ => ({ value: null, state: x }))
Rules first
As with all monads, to be a monad, it must satisfy the following three monad laws.
- left identity:
pure(a).bind(f) == f(a)
- right identity:
m.bind(pure) == m
- associativity:
m.bind(f).bind(g) == m.bind(x => f(x).bind(g))
Where pure
is our way of putting a value into an instance of our Monad, m
, and bind
is our way of interacting with the instance's contained value – You'll sometimes here pure
called return
in other descriptions of monads, but I'll avoid using return
in this answer as it's a keyword in JavaScript and not related to monads.
Don't worry too much about understanding the implementation details. When you're starting out, it's better to develop an intuition about how the State monad works. We'll see that in a moment as we now look at a sample program using the state monad
Your first stateful function
Assuming your initial program state is {}
, we'll look at a function that modifies the state to add the user
and corresponding userId
. This could be the result of looking up a user in the database and we set the authorized
property to false
until we later verify that the user's password is correct
const setUser = userId =>
State.get().bind(settings =>
State.put(Object.assign({}, settings, {
user: { userId, authorized: false }
})))
Step 1 of this function is to get the state using State.get()
– on its own, it seems like it does nothing, but it's in the context where this function is called that it makes a distinct difference. Later on you'll see where we execute the computation with an initial state value which State.get
seemingly pulls out of thin air. Again, for now, just get an intuition for it and assume that somehow we get the state.
Step 2 of this is the bind(settings => ...
bit. Recall bind
is how we interact with our state value, and in this case it's our settings
object. So all we really want to do here is update settings
to include a user
property set to { userId, authorized: false }
. After setUser
is called, we can think of our state having a new shape of
// old state
let settings = {...}
// new state
settings = { ...settings, { user: { userId, authorized: false } } }
A second stateful function
Ok, so your user wants to login now. Let's accept a challenge
and compare it against some password – if the user provided the correct password, we'll update the state to show authorized: true
, otherwise we'll just leave it set to false
const PASSWORD = 'password1'
const attemptLogin = challenge =>
State.get().bind(settings => {
let { userId, authorized } = settings.user
if (challenge === PASSWORD)
return State.put(Object.assign({}, settings, {
user: { userId, authorized: true }
}))
else
return State.pure(settings)
})
Step 1 is to get the state again, just like last time
Step 2 is bind
to get access to the state value and do something. Recall where we left off in the previous state modification (new state
at the end of the last section): to read the user's current state, we want to read settings.user
.
Step 3 is to decide what the next state will be: if the user provided the correct challenge
(ie, it's equal to PASSWORD
), the we will return a new state with authorized
set to true
– otherwise, if the challenge does not match the password, return an unmodified state using State.pure(settings)
Your first stateful program
So now we have two functions (setUser
and attemptLogin
) that read state and return state of their own. Writing our program is easy now
const initialState = {}
const main = (userId, challenge) =>
setUser(userId)
.bind(() => attemptLogin(challenge))
.execState(initialState)
That's it. Run the complete code example below to see output for two login scenarios: one with a valid password, and one with an invalid password
Complete code example
// State monad
const State = runState => ({
runState,
bind: f => State(s => {
let { value, state } = runState(s)
return f(value).runState(state)
}),
evalState: s =>
runState(s).value,
execState: s =>
runState(s).state
})
State.pure = y =>
State(x => ({ value: y, state: x }))
State.get = () =>
State(x => ({ value: x, state: x }))
State.put = x =>
State($ => ({ value: null, state: x }))
// your program
const PASSWORD = 'password1'
const initialState = {}
const setUser = userId =>
State.get().bind(settings =>
State.put(Object.assign({}, settings, {
user: { userId, authorized: false }
})))
const attemptLogin = challenge =>
State.get().bind(settings => {
let { userId, authorized } = settings.user
if (challenge === PASSWORD)
return State.put(Object.assign({}, settings, {
user: { userId, authorized: true }
}))
else
return State.pure(settings)
})
const main = (userId, challenge) =>
setUser(userId)
.bind(() => attemptLogin(challenge))
.execState(initialState)
// good login
console.log(main(5, 'password1'))
// { user: { userId: 5, authorized: true } }
// bad login
console.log(main(5, '1234'))
// { user: { userId: 5, authorized: false } }
Where to go from here?
This just scratches the surface on the state monad. It took me a long time to gain some intuition on how it works, and I still am yet to master it.
If you're scratching your head on how it works, I would strongly encourage you to do the old pen/paper evaluation strategy – trace the program and be meticulous with your substitutions. It's amazing when you see it all come together.
And be sure to do some more reading on State monad at various sources
"better using Ramda?"
I think part of your problem is that you're lacking some of the fundamentals to reason about programs in a functional way. Reaching for a library like Ramda is not likely to help you develop the skill or give you a better intuition. In my own experience, I learn best by sticking to the basics and building up from there.
As a discipline, you could practice implementing any Ramda function before using it. Only then can you truly know/appreciate what Ramda is bringing into the mix.