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