state monad
Maybe you're looking for the state monad. The first example in Peter's post is similar to how the state monad works. The primary difference is state changes do not mutate previous state.
Let's first design the initial state of our project -
const initProject = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }]
}
Now we can write a function that updates the title -
const setTitle = title =>
State.get().bind(project => // get the state, bind to "project"
State.put({...project, title}) // set new state
)
Here's one to create a new tab -
const createTab = title =>
State.get().bind(project => // get the state, bind to "project"
State.put({...project, tabs: [...project.tabs, {title}]}) // set new state
)
The bind
function of any monad allows us to read the contained value, perform some computation with it, and return a new monad encapsulating the result of that computation -
console.log(
setTitle("hello world")
.bind(() => createTab("2"))
.bind(() => createTab("3"))
.execState(initState)
)
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
Let's run a code example to check our progress. Don't worry about understanding State
for now -
// state monad
const State = Object.assign(
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,
}),
{
return: y => State(x => ({value: y, state: x})),
get: () => State(x => ({value: x, state: x})),
put: x => State(_ => ({value: null, state: x})),
},
)
// your methods
const setTitle = title =>
State.get().bind(project =>
State.put({...project, title})
)
const createTab = title =>
State.get().bind(project =>
State.put({...project, tabs: [...project.tabs, {title}]})
)
// your program
const initState = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }],
}
console.log(
setTitle("hello world")
.bind(() => createTab("2"))
.bind(() => createTab("3"))
.execState(initState)
)
console.log(
createTab("⚠️")
.bind(() => createTab("⚠️"))
.execState(initState)
)
.as-console-wrapper { min-height: 100%; top: 0 }
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
{
"title": "<insert title>",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "⚠️"
},
{
"title": "⚠️"
}
]
}
too much .bind!
The .bind(project => ...)
allows you to read the state, similar to how Promise .then(value => ...)
allows you to read the value of a promise, but these closures can be a burden to work with. Much like Promise has async..await
, we can implement State.run
to eliminate need for .bind
closures -
const setTitle = title => State.run(function *() {
const project = yield State.get() // State.get().bind(project => ...
return State.put({...project, title})
})
const createTab = title => State.run(function *() {
const project = yield State.get() // State.get().bind(project => ...
return State.put({...project, tabs: [...project.tabs, {title}]})
})
The benefit is observed when more .bind
calls are saved. If the result of the bind is not needed, you can leave the LHS of yield
empty -
State.run(function *() {
yield setTitle("hello world") // setTitle("hello world").bind(() => ...
yield createTab("2") // createTab("2").bind(() => ...
return createTab("3")
})
This updated demo using State.run
produces the same result without the need for .bind
closures -
// state monad
const State = Object.assign(
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,
}),
{
return: y => State(x => ({value: y, state: x})),
get: () => State(x => ({value: x, state: x})),
put: x => State(_ => ({value: null, state: x})),
run: e => {
const g = e()
const next = x => {
let {done, value} = g.next(x)
return done ? value : value.bind(next)
}
return next()
},
},
)
// your methods
const setTitle = title => State.run(function *() {
const project = yield State.get()
return State.put({...project, title})
})
const createTab = title => State.run(function *() {
const project = yield State.get()
return State.put({...project, tabs: [...project.tabs, {title}]})
})
// your program
const initProject = {
title: "<insert title>",
tabs: [{ title: "Untitled.txt" }],
}
console.log(State.run(function *() {
yield setTitle("hello world")
yield createTab("2")
return createTab("3")
}).execState(initProject))
console.log(State.run(function *() {
yield createTab("⚠️")
return createTab("⚠️")
}).execState(initProject))
.as-console-wrapper { min-height: 100%; top: 0 }
{
"title": "hello world",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "2"
},
{
"title": "3"
}
]
}
{
"title": "<insert title>",
"tabs": [
{
"title": "Untitled.txt"
},
{
"title": "⚠️"
},
{
"title": "⚠️"
}
]
}
related
You may find other useful details about the state monad in an older post.