Search code examples
javascripttypesfunctional-programmingmonadsfantasyland

Why does the fantasy land spec require that chain must return a value of the same Chain?


chain method

A value which has a Chain must provide a chain method. The chain method takes one argument:

m.chain(f)

  1. f must be a function which returns a value
    • If f is not a function, the behaviour of chain is unspecified.
    • f must return a value of the same Chain
  2. chain must return a value of the same Chain

GitHub - fantasyland

Given is a simple implementation of the option monad:

// prototypes:
const someProto = {
  of(x) { return some(x) },
  map(f) { return some(f(this.x)) },
  ap(ftor) { return ftor.map(this.x) },
  join() { return this.x },
  chain(mf) { return this.map(mf).join() }
};

const noneProto = {
  of() { return this },
  map() { return this },
  ap() { return this },
  join() { return this },
  chain() { return this }
};

// factories:
function some(x) {
  return Object.assign(Object.create(someProto), {x: x});
}

function none() {
  return Object.assign(Object.create(noneProto), {x: null});
}

To guarantee that chain always returns an option monad, I'd have to ensure that mf (monadic function) always returns one. This isn't possible, because mf is not part of the implementation. Rather it's defined when the monad is used:

// auxiliary function:
const sub = y => x => x - y;

let a = some(2);
let b = some(3);

a.chain(x => b.chain(y => some(sub(x)(y)))); // {x: 1}
a.chain(x => b.chain(y => sub(x)(y))); // 1 - ouch!

In the second method application the passed function returns no monad, which leads to an unwrapped result of the monadic computation. I could add a type check to chain or join maybe by duck typing, to solve the issue - that would be pretty ugly though.

Why does the specification require type safety exactly at this point? Javascript is dynamically typed and I would prefer to write appropriate unit tests instead of performing type checks at run-time. Would I violate the specification then?


Solution

  • In your second example you should use .map():

    a.chain(x => b.map(y => sub(x)(y))); 
    

    and then everything follows the rules.

    For comparison here are the equivalent Haskell signatures:

    fmap  :: m a -> (a ->   b)   -> m b    -- same as .map()
    (>>=) :: m a -> (a -> m b)   -> m b    -- same as .chain()
    

    So if your function returns a monadic value, use .chain(). Otherwise use .map().