Skip to content

pelotom/burrido

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

36 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

burrido Build Status

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:

  1. 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 many Monad definitions as you want for a particular type (constructor), and each just has its own special Do function.
  2. 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 Observables 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 instructive 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 Observables. Each do* flavor applies its own semantics to the provided block, but they all return Observables, 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 Observables, but it only works properly with single-valued streams (essentially Promises), whereas burrido allows manipulating streams of multiple values, using multiple different semantics.

Caveats

Because JavaScript's generators are neither immutable nor cloneable, we are forced to simulate immutable generators using a library called immutagen. There are two unavoidable downsides to this approach:

  • pure and bind must be side-effect free. This can always be achieved in practice by making the monadic type lazy (wrapping it in a closure). See this issue.
  • The simulation is inefficient, requiring O(n^2) steps to execute the nth yield within a single generator. See this issue.

With natively immutable generators (or the native ability to clone mutable generators), both of these limitations would go away.