For purposes of this document, a “chainable” is any value that:
- Is a wrapper around another value (which can be anything).
- Has a
.map(value => value + 1)
method which takes a function that modifies the wrapped value. - Has a
.chain(value => Wrapper(value + 1))
method which takes a function that returns another wrapper (must be the same wrapper type), and “unwraps” the nested wrapper.
Chainables are a powerful programming pattern that can be used to express a large number of pure solutions to programming problems including but not limited to: asynchronous code, safe null checks, dependency injection, state changes, observables, error handling, and lazy evaluation.
const foo = {bar: null}
const baz =
option(foo)
.chain(foo =>
option(foo.bar).chain(bar =>
option(bar.baz).chain(baz =>
option(baz.biz).map(biz =>
biz
)
)
)
)
Nested chainable calls can quickly become verbose and clunky to deal with. Async/await helps with this, but is specialized to Promises.
The intent behind this nested sequence of code can be expressed clearer with just a small amount of syntax sugar:
const fooOption = option({bar: null})
const baz =
do {
foo <- fooOption
bar <- option(foo.bar)
baz <- option(bar.baz)
biz <- option(baz.biz)
biz
}
The rules for the syntax are rather simple:
Anytime a <-
expression is inside a do
block, it’s split into two sides. The RHS value has a chain
method invoked, and the LHS is presented as the sole parameter to the chain
method.
For the last <-
expression, chain
is replaced with map
, and the very last expression of the do block is returned from this method
do {
{paramA} <- {exprA}
{paramB} <- {exprB}
{exprC}
}
Is de-sugared to:
{exprA}.chain({paramA} => {
return {exprB}.map({paramB} => {
return {exprC}
})
})
Since the only requirements for this are that a value has a .chain
and .map
method, it can work for all kinds of chainables included but not limitied to:
Asynchronous chainables:
do {
user <- fetchUser('paul')
img <- fetchImage(user.profileImage)
friend <- fetchUser(user.friend_id)
{...user, img, friend}
}
Option (Maybe) chainables:
const baz =
do {
posts <- option(user.posts)
firstPost <- option(posts[0])
firstPost.likeCount / posts.length
}
Error handling chainables:
const baz =
do {
count1 <- Try(() => parseInt('15'))
count2 <- Try(() => parseInt('30'))
count1 * count2
}
These are just some examples. There are many more!
This babel plugin implements this proposal with a few caveats:
<<
is used instead of<-
.- A destructuring expression doesn’t work as the LHS of a
<<
expression.
These caveats are simply because this is implemented as a babel plugin. An update to babel would be needed to add the <-
operator and support destructuring on the LHS.
Scala has for comprehensions
for {
n1 <- fetchNumber(...)
n2 <- fetchNumber(...)
n3 <- fetchNumber(...)
} yield (n1 + n2 + n3)
Haskell has do notation
do { x1 <- action1
; x2 <- action2
; mk_action3 x1 x2 }