burrido
v1.0.8
Published
Monadic do-notation for JavaScript
Downloads
273
Maintainers
Readme
burrido
An experiment in bringing Haskell's programmable semicolon to JavaScript, using generators.
Installation
npm install --save burrido
Usage
import Monad from 'burrido'
const ArrayMonad = Monad({
pure: (x) => [x],
bind: (xs, f) => xs.map(f).reduce((a, b) => a.concat(b), [])
})
ArrayMonad.Do(function*() {
const x = yield [1,2]
const y = yield [3,4]
return x * y
}) // -> [3,4,6,8]
The above should look fairly self-explanatory to a Haskell programmer: we are declaring a Monad
instance for arrays, which requires us to define two functions: pure
and bind
. Then we obtain a special function Do
which is a do-notation tailored to that particular monad. We pass a generator function to Do
, within which we gain access to the yield
keyword, allowing us to "unwrap" monadic values and release their effects.
In fact this is a bit more versatile than Haskell's do-notation in a couple of interesting ways:
- Haskell's
Monad
is a type class, which means that there can only be one way in which a given type constructor is considered a monad within a given scope. But some type constructors can be considered monadic in more than one way (e.g.Either
). By contrast, here you can create as manyMonad
definitions as you want for a particular type (constructor), and each just has its own specialDo
function. - While
const foo = yield bar
is comparable to
foo <- bar
in do-notation, one can also create compound yield
expressions which have no direct analogue in Haskell. For example,
const foo = yield (yield bar)
would have to be written as
foo' <- bar
foo <- foo'
in do-notation. In the context of Do
blocks, yield
serves a similar purpose to the !
operator in both Idris and the Effectful library for Scala.
An example using RxJS
RxJS Observable
s form a monad in several different ways:
const { just: pure } = Observable
const { Do: doConcat } = Monad({
pure,
bind: (x, f) => x.concatMap(f)
})
const { Do: doMerge } = Monad({
pure,
bind: (x, f) => x.flatMap(f)
})
const { Do: doLatest } = Monad({
pure,
bind: (x, f) => x.flatMapLatest(f)
})
It's insructive to see what happens when you apply these different do-notations to the same generator block:
const { from } = Observable
const block = function*() {
// for each x in [1,2,3]...
const x = yield from([1,2,3])
// wait 1 second
yield pure({}).delay(1000)
// then return the value
return x
}
// Prints 1, 2, and 3 separated by 1 second intervals
doConcat(block).subscribe(console.log)
// Waits 1 second and then prints 1, 2, 3 all at once
doMerge(block).subscribe(console.log)
// Waits 1 second and then prints 3
doLatest(block).subscribe(console.log)
This should make sense if you think about the semantics of each of these different methods of "flattening" nested Observable
s. Each do*
flavor applies its own semantics to the provided block, but they all return Observable
s, so we can freely combine them:
doConcat(function*() {
const x = yield doConcat(function*() {
//...
}),
y = yield doMerge(function*() {
//...
}),
z = yield doLatest(function*() {
//...
})
return { x, y, z }
})
RxJS has a function spawn
which allows you to use this kind of syntax with Observable
s, but it only works properly with single-valued streams (essentially Promises), whereas burrido allows manipulating streams of multiple values, using multiple different semantics.