import {Monad, monad} from "jabz/monad";
import {
Observer, MultiObserver, noopObserver
} from "./frp-common";
import {Future, BehaviorFuture} from "./future";
import * as F from "./future";
import {Stream} from "./stream";
class IncompleteObserver implements Observer {
beginPulling(): void {
throw new Error("beginPulling not implemented");
}
endPulling(): void {
throw new Error("beginPushing not implemented");
}
push(a: A): void {
throw new Error("push not implemented");
}
}
class OnlyPushObserver implements Observer {
constructor(private cb: (a: A) => void) {};
beginPulling(): void {}
endPulling(): void {}
push(a: A): void {
this.cb(a);
}
}
/**
* A behavior is a value that changes over time. Conceptually it can
* be though of as a function from time to a value. I.e. `type
* Behavior = (t: Time) => A`.
*/
@monad
export abstract class Behavior implements Observer, Monad {
pushing: boolean;
// Behaviors that are pushing caches their last value in `last`. For
// behaviors that pull `last` is unused.
last: A;
nrOfListeners: number;
child: Observer;
abstract push(a: any): void;
abstract pull(): A;
constructor() {
this.child = noopObserver;
this.nrOfListeners = 0;
}
map(fn: (a: A) => B): Behavior {
const newB = new MapBehavior(this, fn);
this.addListener(newB);
return newB;
}
mapTo(v: A): Behavior {
return new ConstantBehavior(v);
}
static of(v: A): Behavior {
return new ConstantBehavior(v);
};
of(v: A): Behavior {
return new ConstantBehavior(v);
}
ap(f: Behavior<(a: A) => B>): Behavior {
const newB = new ApBehavior(f, this);
f.addListener(newB);
this.addListener(newB);
return newB;
}
lift(f: (t: T1) => R, m: Behavior): Behavior;
lift(f: (t: T1, u: T2) => R, m1: Behavior, m2: Behavior): Behavior;
lift(f: (t1: T1, t2: T2, t3: T3) => R, m1: Behavior, m2: Behavior, m3: Behavior): Behavior;
lift(/* arguments */): any {
// TODO: Experiment with faster specialized `lift` implementation
const f = arguments[0];
switch (arguments.length - 1) {
case 1:
return arguments[1].map(f);
case 2:
return arguments[2].ap(arguments[1].map((a: any) => (b: any) => f(a, b)));
case 3:
return arguments[3].ap(arguments[2].ap(arguments[1].map(
(a: any) => (b: any) => (c: any) => f(a, b, c)
)));
}
}
static multi: boolean = false;
multi: boolean = false;
chain(fn: (a: A) => Behavior): Behavior {
return new ChainBehavior(this, fn);
}
flatten: () => Behavior;
endPulling(): void {
this.pushing = true;
this.child.endPulling();
}
beginPulling(): void {
this.pushing = false;
this.child.beginPulling();
}
subscribe(cb: (a: A) => void): Observer {
const listener = new OnlyPushObserver(cb);
this.addListener(listener);
cb(at(this));
return listener;
}
addListener(c: Observer): void {
const nr = ++this.nrOfListeners;
if (nr === 1) {
this.child = c;
} else if (nr === 2) {
this.child = new MultiObserver(this.child, c);
} else {
(>this.child).listeners.push(c);
}
}
removeListener(listener: Observer): void {
const nr = --this.nrOfListeners;
if (nr === 0) {
this.child = noopObserver;
} else if (nr === 1) {
const l = (>this.child).listeners;
this.child = l[l[0] === listener ? 1 : 0];
} else {
const l = (>this.child).listeners;
// The indexOf here is O(n), where n is the number of listeners,
// if using a linked list it should be possible to perform the
// unsubscribe operation in constant time.
const idx = l.indexOf(listener);
if (idx !== -1) {
if (idx !== l.length - 1) {
l[idx] = l[l.length - 1];
}
l.length--; // remove the last element of the list
}
}
}
observe(
push: (a: A) => void,
beginPulling: () => void,
endPulling: () => void,
): CbObserver {
return new CbObserver(push, beginPulling, endPulling, this);
}
at(): A {
return this.pushing === true ? this.last : this.pull();
}
}
/*
* Impure function that gets the current value of a behavior. For a
* pure variant see `sample`.
*/
export function at(b: Behavior): B {
return b.at();
}
/** @private */
class ConstantBehavior extends Behavior {
constructor(public last: A) {
super();
this.pushing = true;
}
push(): void {
throw new Error("Cannot push a value to a constant behavior");
}
pull(): A {
return this.last;
}
}
/** @private */
class MapBehavior extends Behavior {
constructor(
private parent: Behavior,
private fn: (a: A) => B
) {
super();
this.pushing = parent.pushing;
if (this.pushing === true) {
this.last = fn(at(parent));
}
}
push(a: A): void {
this.last = this.fn(a);
this.child.push(this.last);
}
pull(): B {
return this.fn(at(this.parent));
}
}
/** @private */
class ChainOuter extends IncompleteObserver {
constructor(private chainB: ChainBehavior) {
super();
};
push(a: A): void {
this.chainB.pushOuter(a);
}
}
/** @private */
class ChainBehavior extends Behavior {
// The last behavior returned by the chain function
private innerB: Behavior;
private outerConsumer: Observer;
constructor(
private outer: Behavior,
private fn: (a: A) => Behavior
) {
super();
// Create the outer consumer
this.outerConsumer = new ChainOuter(this);
// Make the consumers listen to inner and outer behavior
outer.addListener(this.outerConsumer);
if (outer.pushing === true) {
this.innerB = this.fn(at(this.outer));
this.pushing = this.innerB.pushing;
this.innerB.addListener(this);
this.last = at(this.innerB);
}
}
pushOuter(a: A): void {
// The outer behavior has changed. This means that we will have to
// call our function, which will result in a new inner behavior.
// We therefore stop listening to the old inner behavior and begin
// listening to the new one.
if (this.innerB !== undefined) {
this.innerB.removeListener(this);
}
const newInner = this.innerB = this.fn(a);
this.pushing = newInner.pushing;
newInner.addListener(this);
this.push(at(newInner));
}
push(b: B): void {
this.last = b;
this.child.push(b);
}
pull(): B {
return at(this.fn(at(this.outer)));
}
}
/** @private */
class FunctionBehavior extends Behavior {
constructor(private fn: () => A) {
super();
this.pushing = false;
}
push(v: A): void {
throw new Error("Cannot push to a FunctionBehavior");
}
pull(): A {
return this.fn();
}
}
/** @private */
class ApBehavior extends Behavior {
last: B;
constructor(
private fn: Behavior<(a: A) => B>,
private val: Behavior
) {
super();
this.pushing = fn.pushing && val.pushing;
if (this.pushing) {
this.last = at(fn)(at(val));
}
}
push(): void {
const fn = at(this.fn);
const val = at(this.val);
this.last = fn(val);
this.child.push(this.last);
}
pull(): B {
return at(this.fn)(at(this.val));
}
}
/**
* Apply a function valued behavior to a value behavior.
*
* @param fnB behavior of functions from `A` to `B`
* @param valB A behavior of `A`
* @returns Behavior of the function in `fnB` applied to the value in `valB`
*/
export function ap(fnB: Behavior<(a: A) => B>, valB: Behavior): Behavior {
return valB.ap(fnB);
}
/** @private */
class SinkBehavior extends Behavior {
constructor(public last: B) {
super();
this.pushing = true;
}
push(v: B): void {
if (this.last !== v) {
this.last = v;
this.child.push(v);
}
}
pull(): B {
return this.last;
}
}
/**
* Creates a behavior for imperative impure pushing.
*/
export function sink(initialValue: A): Behavior {
return new SinkBehavior(initialValue);
}
/**
* A placeholder behavior is a behavior without any value. It is used
* to do value recursion in `./framework.ts`.
* @private
*/
export class PlaceholderBehavior extends Behavior {
private source: Behavior;
constructor() {
super();
// `undefined` indicates that this behavior is neither pushing nor
// pulling
this.pushing = undefined;
}
push(v: B): void {
this.last = v;
this.child.push(v);
}
pull(): B {
return this.source.pull();
}
replaceWith(b: Behavior): void {
this.source = b;
b.addListener(this);
this.pushing = b.pushing;
if (b.pushing === true) {
this.push(at(b));
} else {
this.beginPulling();
}
}
}
export function placeholder(): PlaceholderBehavior {
return new PlaceholderBehavior();
}
/** @private */
class WhenBehavior extends Behavior> {
constructor(private parent: Behavior) {
super();
this.pushing = true;
parent.addListener(this);
this.push(at(parent));
}
push(val: boolean): void {
if (val === true) {
this.last = Future.of({});
} else {
this.last = new BehaviorFuture(this.parent);
}
}
pull(): Future<{}> {
return this.last;
}
}
export function when(b: Behavior): Behavior> {
return new WhenBehavior(b);
}
// FIXME: This can probably be made less ugly.
/** @private */
class SnapshotBehavior extends Behavior> {
private afterFuture: boolean;
constructor(private parent: Behavior, future: Future) {
super();
if (future.occured === true) {
// Future has occurred at some point in the past
this.afterFuture = true;
this.pushing = parent.pushing;
parent.addListener(this);
this.last = Future.of(at(parent));
} else {
this.afterFuture = false;
this.pushing = true;
this.last = F.sinkFuture();
future.listen(this);
}
}
push(val: any): void {
if (this.afterFuture === false) {
// The push is coming from the Future, it has just occurred.
this.afterFuture = true;
this.last.resolve(at(this.parent));
this.parent.addListener(this);
} else {
// We are recieving an update from `parent` after `future` has
// occurred.
this.last = Future.of(val);
}
}
pull(): Future {
return this.last;
}
}
export function snapshotAt(
b: Behavior, f: Future
): Behavior> {
return new SnapshotBehavior(b, f);
}
/** @private */
class SwitcherBehavior extends Behavior {
constructor(
private b: Behavior,
next: Future> | Stream>) {
super();
this.pushing = b.pushing;
if (this.pushing === true) {
this.last = at(b);
}
b.addListener(this);
// FIXME: Using `bind` is hardly optimal for performance.
next.subscribe(this.doSwitch.bind(this));
}
push(val: A): void {
this.last = val;
this.child.push(val);
}
pull(): A {
return at(this.b);
}
private doSwitch(newB: Behavior): void {
this.b.removeListener(this);
this.b = newB;
newB.addListener(this);
if (newB.pushing === true) {
if (this.pushing === false) {
this.endPulling();
}
this.push(at(newB));
} else if (this.pushing === true) {
this.beginPulling();
}
}
}
export function switchTo(
init: Behavior,
next: Future>
): Behavior {
return new SwitcherBehavior(init, next);
}
export function switcher(
init: Behavior, stream: Stream>
): Behavior> {
return fromFunction(() => new SwitcherBehavior(init, stream));
}
/** @private */
class StepperBehavior extends Behavior {
constructor(initial: B, private steps: Stream) {
super();
this.pushing = true;
this.last = initial;
steps.addListener(this);
}
push(val: B): void {
this.last = val;
this.child.push(val);
}
pull(): B {
throw new Error("Cannot pull from StepperBehavior");
}
}
export function stepper(initial: B, steps: Stream): Behavior {
return new StepperBehavior(initial, steps);
}
/** @private */
class ScanBehavior extends Behavior {
constructor(initial: B,
private fn: (a: A, b: B) => B,
private source: Stream) {
super();
this.pushing = true;
this.last = initial;
source.addListener(this);
}
push(val: A): void {
this.last = this.fn(val, this.last);
this.child.push(this.last);
}
pull(): B {
throw new Error("Cannot pull from Scan");
}
}
export function scan(fn: (a: A, b: B) => B, init: B, source: Stream): Behavior> {
return fromFunction(() => new ScanBehavior(init, fn, source));
}
export function toggle(
initial: boolean, turnOn: Stream, turnOff: Stream
): Behavior {
return stepper(initial, turnOn.mapTo(true).combine(turnOff.mapTo(false)));
}
export function fromFunction(fn: () => B): Behavior {
return new FunctionBehavior(fn);
}
export class CbObserver implements Observer {
constructor(
private _push: (a: A) => void,
private _beginPulling: () => void,
private _endPulling: () => void,
private source: Behavior
) {
source.addListener(this);
// We explicitly checks for both `true` and `false` because
// placeholder behavior is `undefined`
if (source.pushing === false) {
_beginPulling();
} else if (source.pushing === true) {
_push(source.last);
}
}
push(a: A): void {
this._push(a);
}
beginPulling(): void {
this._beginPulling();
}
endPulling(): void {
this._endPulling();
}
}
/**
* Observe a behavior for the purpose of executing imperative actions
* based on the value of the behavior.
*/
export function observe(
push: (a: A) => void,
beginPulling: () => void,
endPulling: () => void,
b: Behavior
): CbObserver {
return b.observe(push, beginPulling, endPulling);
}
/**
* Imperatively push a value into a behavior.
*/
export function publish(a: A, b: Behavior): void {
b.push(a);
}
export function isBehavior(b: any): b is Behavior {
return typeof b === "object" && ("observe" in b) && ("at" in b);
}
export type Time = number;
class TimeFromBehavior extends Behavior