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 }