import { Lambda1, Lambda1_deps, Lambda1_toFunction, Lambda2, Lambda2_deps, Lambda2_toFunction, Lambda3, Lambda3_deps, Lambda3_toFunction, Lambda4, Lambda4_deps, Lambda4_toFunction, Lambda5, Lambda5_deps, Lambda5_toFunction, Lambda6, Lambda6_deps, Lambda6_toFunction, toSources, lambda1 } from "./Lambda"; import { Source, Vertex } from "./Vertex"; import { Transaction } from "./Transaction"; import { Lazy } from "./Lazy"; import { Listener } from "./Listener"; import { Stream, StreamWithSend } from "./Stream"; import { Operational } from "./Operational"; import { Tuple2 } from "./Tuple2"; class LazySample { constructor(cell : Cell) { this.cell = cell; } cell : Cell; hasValue : boolean = false; value : A = null; } class ApplyState { constructor() {} f : (a : A) => B = null; f_present : boolean = false; a : A = null; a_present : boolean = false; } export class Cell { private str : Stream; protected value : A; protected valueUpdate : A; private cleanup : () => void; protected lazyInitValue : Lazy; // Used by LazyCell private vertex : Vertex; constructor(initValue : A, str? : Stream) { this.value = initValue; if (!str) { this.str = new Stream(); this.vertex = new Vertex("ConstCell", 0, []); } else Transaction.run(() => this.setStream(str)); } protected setStream(str : Stream) { this.str = str; const me = this, src = new Source( str.getVertex__(), () => { return str.listen_(me.vertex, (a : A) => { if (me.valueUpdate == null) { Transaction.currentTransaction.last(() => { me.value = me.valueUpdate; me.lazyInitValue = null; me.valueUpdate = null; }); } me.valueUpdate = a; }, false); } ); this.vertex = new Vertex("Cell", 0, [src]); // We do a trick here of registering the source for the duration of the current // transaction so that we are guaranteed to catch any stream events that // occur in the same transaction. // // A new temporary vertex null is constructed here as a performance work-around to avoid // having too many children in Vertex.NULL as a deregister operation is O(n^2) where // n is the number of children in the vertex. let tmpVertexNULL = new Vertex("Cell::setStream", 1e12, []); this.vertex.register(tmpVertexNULL); Transaction.currentTransaction.last(() => { this.vertex.deregister(tmpVertexNULL); }); } getVertex__() : Vertex { return this.vertex; } getStream__() : Stream { // TO DO: Figure out how to hide this return this.str; } /** * Sample the cell's current value. *

* It should generally be avoided in favour of {@link listen(Handler)} so you don't * miss any updates, but in many circumstances it makes sense. *

* NOTE: In the Java and other versions of Sodium, using sample() inside map(), filter() and * merge() is encouraged. In the Javascript/Typescript version, not so much, for the * following reason: The memory management is different in the Javascript version, and this * requires us to track all dependencies. In order for the use of sample() inside * a closure to be correct, the cell that was sample()d inside the closure would have to be * declared explicitly using the helpers lambda1(), lambda2(), etc. Because this is * something that can be got wrong, we don't encourage this kind of use of sample() in * Javascript. Better and simpler to use snapshot(). *

* NOTE: If you need to sample() a cell, you have to make sure it's "alive" in terms of * memory management or it will ignore updates. To make a cell work correctly * with sample(), you have to ensure that it's being used. One way to guarantee this is * to register a dummy listener on the cell. It will also work to have it referenced * by something that is ultimately being listened to. */ sample() : A { return Transaction.run(() => { return this.sampleNoTrans__(); }); } sampleNoTrans__() : A { // TO DO figure out how to hide this return this.value; } /** * A variant of {@link sample()} that works with {@link CellLoop}s when they haven't been looped yet. * It should be used in any code that's general enough that it could be passed a {@link CellLoop}. * @see Stream#holdLazy(Lazy) Stream.holdLazy() */ sampleLazy() : Lazy { const me = this; return Transaction.run(() => me.sampleLazyNoTrans__()); } sampleLazyNoTrans__() : Lazy { // TO DO figure out how to hide this const me = this, s = new LazySample(me); Transaction.currentTransaction.sample(() => { s.value = me.valueUpdate != null ? me.valueUpdate : me.sampleNoTrans__(); s.hasValue = true; s.cell = null; }); return new Lazy(() => { if (s.hasValue) return s.value; else return s.cell.sample(); }); } /** * Transform the cell's value according to the supplied function, so the returned Cell * always reflects the value of the function applied to the input Cell's value. * @param f Function to apply to convert the values. It must be referentially transparent. */ map(f : ((a : A) => B) | Lambda1) : Cell { const c = this; return Transaction.run(() => Operational.updates(c).map(f).holdLazy(c.sampleLazy().map(Lambda1_toFunction(f))) ); } /** * Lift a binary function into cells, so the returned Cell always reflects the specified * function applied to the input cells' values. * @param fn Function to apply. It must be referentially transparent. */ lift(b : Cell, fn0 : ((a : A, b : B) => C) | Lambda2) : Cell { const fn = Lambda2_toFunction(fn0), cf = this.map((aa : A) => (bb : B) => fn(aa, bb)); return Cell.apply(cf, b, toSources(Lambda2_deps(fn0))); } /** * Lift a ternary function into cells, so the returned Cell always reflects the specified * function applied to the input cells' values. * @param fn Function to apply. It must be referentially transparent. */ lift3(b : Cell, c : Cell, fn0 : ((a : A, b : B, c : C) => D) | Lambda3) : Cell { const fn = Lambda3_toFunction(fn0), mf : (aa : A) => (bb : B) => (cc : C) => D = (aa : A) => (bb : B) => (cc : C) => fn(aa, bb, cc), cf = this.map(mf); return Cell.apply( Cell.apply D>(cf, b), c, toSources(Lambda3_deps(fn0))); } /** * Lift a quaternary function into cells, so the returned Cell always reflects the specified * function applied to the input cells' values. * @param fn Function to apply. It must be referentially transparent. */ lift4(b : Cell, c : Cell, d : Cell, fn0 : ((a : A, b : B, c : C, d : D) => E) | Lambda4) : Cell { const fn = Lambda4_toFunction(fn0), mf : (aa : A) => (bb : B) => (cc : C) => (dd : D) => E = (aa : A) => (bb : B) => (cc : C) => (dd : D) => fn(aa, bb, cc, dd), cf = this.map(mf); return Cell.apply( Cell.apply( Cell.apply (d : D) => E>(cf, b), c), d, toSources(Lambda4_deps(fn0))); } /** * Lift a 5-argument function into cells, so the returned Cell always reflects the specified * function applied to the input cells' values. * @param fn Function to apply. It must be referentially transparent. */ lift5(b : Cell, c : Cell, d : Cell, e : Cell, fn0 : ((a : A, b : B, c : C, d : D, e : E) => F) | Lambda5) : Cell { const fn = Lambda5_toFunction(fn0), mf : (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => F = (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => fn(aa, bb, cc, dd, ee), cf = this.map(mf); return Cell.apply( Cell.apply( Cell.apply( Cell.apply (d : D) => (e : E) => F>(cf, b), c), d), e, toSources(Lambda5_deps(fn0))); } /** * Lift a 6-argument function into cells, so the returned Cell always reflects the specified * function applied to the input cells' values. * @param fn Function to apply. It must be referentially transparent. */ lift6(b : Cell, c : Cell, d : Cell, e : Cell, f : Cell, fn0 : ((a : A, b : B, c : C, d : D, e : E, f : F) => G) | Lambda6) : Cell { const fn = Lambda6_toFunction(fn0), mf : (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => (ff : F) => G = (aa : A) => (bb : B) => (cc : C) => (dd : D) => (ee : E) => (ff : F) => fn(aa, bb, cc, dd, ee, ff), cf = this.map(mf); return Cell.apply( Cell.apply( Cell.apply( Cell.apply( Cell.apply (d : D) => (e : E) => (f : F) => G>(cf, b), c), d), e), f, toSources(Lambda6_deps(fn0))); } /** * High order depenency traking. If any newly created sodium objects within a value of a cell of a sodium object * happen to accumulate state, this method will keep the accumulation of state up to date. */ public tracking(extractor: (a: A) => (Stream|Cell)[]) : Cell { const out = new StreamWithSend(null); let vertex = new Vertex("tracking", 0, [ new Source( this.vertex, () => { let cleanup2: ()=>void = () => {}; let updateDeps = (a: A) => { let lastCleanups2 = cleanup2; let deps = extractor(a).map(dep => dep.getVertex__()); for (let i = 0; i < deps.length; ++i) { let dep = deps[i]; vertex.childrn.push(dep); dep.increment(Vertex.NULL); } cleanup2 = () => { for (let i = 0; i < deps.length; ++i) { let dep = deps[i]; for (let j = 0; j < vertex.childrn.length; ++j) { if (vertex.childrn[j] === dep) { vertex.childrn.splice(j, 1); break; } } dep.decrement(Vertex.NULL); } }; lastCleanups2(); }; updateDeps(this.sample()); var cleanup1 = Operational.updates(this).listen_( vertex, (a: A) => { updateDeps(a); out.send_(a); }, false ); return () => { cleanup1(); cleanup2(); } } ) ]); out.setVertex__(vertex); return out.holdLazy(this.sampleLazy()); } /** * Lift an array of cells into a cell of an array. */ public static liftArray(ca : Cell[]) : Cell { return Cell._liftArray(ca, 0, ca.length); } private static _liftArray(ca : Cell[], fromInc: number, toExc: number) : Cell { if (toExc - fromInc == 0) { return new Cell([]); } else if (toExc - fromInc == 1) { return ca[fromInc].map(a => [a]); } else { let pivot = Math.floor((fromInc + toExc) / 2); // the thunk boxing/unboxing here is a performance hack for lift when there are simutaneous changing cells. return Cell._liftArray(ca, fromInc, pivot).lift( Cell._liftArray(ca, pivot, toExc), (array1, array2) => () => array1.concat(array2) ) .map(x => x()); } } /** * Apply a value inside a cell to a function inside a cell. This is the * primitive for all function lifting. */ static apply(cf : Cell<(a : A) => B>, ca : Cell, sources? : Source[]) : Cell { return Transaction.run(() => { let pumping = false; const state = new ApplyState(), out = new StreamWithSend(), cf_updates = Operational.updates(cf), ca_updates = Operational.updates(ca), pump = () => { if (pumping) { return; } pumping = true; Transaction.currentTransaction.prioritized(out.getVertex__(), () => { let f = state.f_present ? state.f : cf.sampleNoTrans__(); let a = state.a_present ? state.a : ca.sampleNoTrans__(); out.send_(f(a)); pumping = false; }); }, src1 = new Source( cf_updates.getVertex__(), () => { return cf_updates.listen_(out.getVertex__(), (f : (a : A) => B) => { state.f = f; state.f_present = true; pump(); }, false); } ), src2 = new Source( ca_updates.getVertex__(), () => { return ca_updates.listen_(out.getVertex__(), (a : A) => { state.a = a; state.a_present = true; pump(); }, false); } ); out.setVertex__(new Vertex("apply", 0, [src1, src2].concat(sources ? sources : []) )); return out.holdLazy(new Lazy(() => cf.sampleNoTrans__()(ca.sampleNoTrans__()) )); }); } /** * Unwrap a cell inside another cell to give a time-varying cell implementation. */ static switchC(cca : Cell>) : Cell { return Transaction.run(() => { const za = cca.sampleLazy().map((ba : Cell) => ba.sample()), out = new StreamWithSend(); let outValue: A = null; let pumping = false; const pump = () => { if (pumping) { return; } pumping = true; Transaction.currentTransaction.prioritized(out.getVertex__(), () => { out.send_(outValue); outValue = null; pumping = false; }); }; let last_ca : Cell = null; const cca_value = Operational.value(cca), src = new Source( cca_value.getVertex__(), () => { let kill2 : () => void = last_ca === null ? null : Operational.value(last_ca).listen_(out.getVertex__(), (a : A) => { outValue = a; pump(); }, false); const kill1 = cca_value.listen_(out.getVertex__(), (ca : Cell) => { last_ca = ca; // Connect before disconnect to avoid memory bounce, when switching to same cell twice. let nextKill2 = Operational.value(ca).listen_(out.getVertex__(), (a : A) => { outValue = a; pump(); }, false); if (kill2 !== null) kill2(); kill2 = nextKill2; }, false); return () => { kill1(); kill2(); }; } ); out.setVertex__(new Vertex("switchC", 0, [src])); return out.holdLazy(za); }); } /** * Unwrap a stream inside a cell to give a time-varying stream implementation. */ static switchS(csa : Cell>) : Stream { return Transaction.run(() => { const out = new StreamWithSend(), h2 = (a : A) => { out.send_(a); }, src = new Source( csa.getVertex__(), () => { let kill2 = csa.sampleNoTrans__().listen_(out.getVertex__(), h2, false); const kill1 = csa.getStream__().listen_(out.getVertex__(), (sa : Stream) => { // Connect before disconnect to avoid memory bounce, when switching to same stream twice. let nextKill2 = sa.listen_(out.getVertex__(), h2, true); kill2(); kill2 = nextKill2; }, false); return () => { kill1(); kill2(); }; } ); out.setVertex__(new Vertex("switchS", 0, [src])); return out; }); } /** * When transforming a value from a larger type to a smaller type, it is likely for duplicate changes to become * propergated. This function insures only distinct changes get propergated. */ calm(eq: (a:A,b:A)=>boolean): Cell { return Operational .updates(this) .collectLazy( this.sampleLazy(), (newValue, oldValue) => { let result: A; if (eq(newValue, oldValue)) { result = null; } else { result = newValue; } return new Tuple2(result, newValue); } ) .filterNotNull() .holdLazy(this.sampleLazy()); } /** * This function is the same as calm, except you do not need to pass an eq function. This function will use (===) * as its eq function. I.E. calling calmRefEq() is the same as calm((a,b) => a === b). */ calmRefEq(): Cell { return this.calm((a, b) => a === b); } /** * Listen for updates to the value of this cell. This is the observer pattern. The * returned {@link Listener} has a {@link Listener#unlisten()} method to cause the * listener to be removed. This is an OPERATIONAL mechanism is for interfacing between * the world of I/O and for FRP. * @param h The handler to execute when there's a new value. * You should make no assumptions about what thread you are called on, and the * handler should not block. You are not allowed to use {@link CellSink#send(Object)} * or {@link StreamSink#send(Object)} in the handler. * An exception will be thrown, because you are not meant to use this to create * your own primitives. */ listen(h : (a : A) => void) : () => void { return Transaction.run(() => { return Operational.value(this).listen(h); }); } /** * Fantasy-land Algebraic Data Type Compatability. * Cell satisfies the Functor, Apply, Applicative categories * @see {@link https://github.com/fantasyland/fantasy-land} for more info */ //of :: Applicative f => a -> f a static 'fantasy-land/of'(a:A):Cell { return new Cell(a); } //map :: Functor f => f a ~> (a -> b) -> f b 'fantasy-land/map'(f : ((a : A) => B)) : Cell { return this.map(f); } //ap :: Apply f => f a ~> f (a -> b) -> f b 'fantasy-land/ap'(cf: Cell<(a : A) => B>):Cell { return Cell.apply(cf, this); } }