import { compose } from '../util';
import { left, right, Left, Either } from './Either';
import { Monad } from './Monad';
import { Functor } from '../data/Functor';
/**
* free wraps a value in a free
*/
export const free = (a: A) => new Return(a);
/**
* suspend lifts a function into a Free monad to mimic tail call recursion.
*/
export const suspend = (f: () => A) => new Suspend(compose(free, f));
/**
* liftF lifts a Functor into a Free.
*/
export const liftF = , A>(f: F) => new Suspend(f.map(free));
/**
* Free is a Free monad that also implements a Free Applicative (almost).
*
* Inspired by https://cwmyers.github.io/monet.js/#free
*/
export abstract class Free implements Monad {
static free = free;
static suspend = suspend;
static liftF = liftF;
/**
* of
*/
of(a: A): Free {
return new Return(a);
}
/**
* map
*/
map(f: (a: A) => B): Free {
return this.chain((a: A) => free(f(a)));
}
/**
* chain
*/
chain(g: (a: A) => Free): Free {
if (this instanceof Suspend) {
let f = this.f;
return (typeof f === 'function') ?
new Suspend((x: A) => f(x).chain(g)) :
new Suspend(f.map((free: Free) => free.chain(g)));
} else if (this instanceof Return) {
g(this.a);
}
}
/**
* resume the next stage of the computation
*/
resume(): Either {
if (this instanceof Suspend) {
return left(this.f)
} else if (this instanceof Return) {
return right(this.a);
}
}
/**
* hoist
hoist(func: (fb: Functor) => Functor): Free {
if (this instanceof Suspend) {
return new Suspend((func(this.f))
.map((fr: Free) => fr.hoist(func)))
} else {
return this;
}
}
*/
/**
* cata
*/
cata(f: (f: F) => B, g: (a: A) => B): B {
return this.resume().cata(f, g);
}
/**
* go runs the computation to completion using f to extract each stage.
* @summmary go :: Free, A> → (F> → Free) → A
*/
go(f: (next: F) => Free): A {
if (this instanceof Suspend) {
let r = this.resume();
while (r instanceof Left)
r = (f(r.takeLeft())).resume();
return r.takeRight();
} else if (this instanceof Return) {
return this.a;
}
}
/**
* run the Free chain to completion
* @summary run :: Free → A
*/
run(): A {
return this.go((next: F): Free => (next)());
}
}
export class Suspend extends Free {
f: F;
constructor(f: F) {
super();
this.f = f;
}
}
export class Return extends Free {
a: A;
constructor(a: A) {
super();
this.a = a;
}
}