95 | * It should generally be avoided in favour of {@link listen(Handler)} so you don't 96 | * miss any updates, but in many circumstances it makes sense. 97 | *
98 | * NOTE: In the Java and other versions of Sodium, using sample() inside map(), filter() and 99 | * merge() is encouraged. In the Javascript/Typescript version, not so much, for the 100 | * following reason: The memory management is different in the Javascript version, and this 101 | * requires us to track all dependencies. In order for the use of sample() inside 102 | * a closure to be correct, the cell that was sample()d inside the closure would have to be 103 | * declared explicitly using the helpers lambda1(), lambda2(), etc. Because this is 104 | * something that can be got wrong, we don't encourage this kind of use of sample() in 105 | * Javascript. Better and simpler to use snapshot(). 106 | *
107 | * NOTE: If you need to sample() a cell, you have to make sure it's "alive" in terms of
108 | * memory management or it will ignore updates. To make a cell work correctly
109 | * with sample(), you have to ensure that it's being used. One way to guarantee this is
110 | * to register a dummy listener on the cell. It will also work to have it referenced
111 | * by something that is ultimately being listened to.
112 | */
113 | sample() : A {
114 | return Transaction.run(() => { return this.sampleNoTrans__(); });
115 | }
116 |
117 | sampleNoTrans__() : A { // TO DO figure out how to hide this
118 | return this.value;
119 | }
120 |
121 | /**
122 | * A variant of {@link sample()} that works with {@link CellLoop}s when they haven't been looped yet.
123 | * It should be used in any code that's general enough that it could be passed a {@link CellLoop}.
124 | * @see Stream#holdLazy(Lazy) Stream.holdLazy()
125 | */
126 | sampleLazy() : Lazy {
127 | const me = this;
128 | return Transaction.run(() => me.sampleLazyNoTrans__());
129 | }
130 |
131 | sampleLazyNoTrans__() : Lazy { // TO DO figure out how to hide this
132 | const me = this,
133 | s = new LazySample(me);
134 | Transaction.currentTransaction.sample(() => {
135 | s.value = me.valueUpdate != null ? me.valueUpdate : me.sampleNoTrans__();
136 | s.hasValue = true;
137 | s.cell = null;
138 | });
139 | return new Lazy(() => {
140 | if (s.hasValue)
141 | return s.value;
142 | else
143 | return s.cell.sample();
144 | });
145 | }
146 |
147 | /**
148 | * Transform the cell's value according to the supplied function, so the returned Cell
149 | * always reflects the value of the function applied to the input Cell's value.
150 | * @param f Function to apply to convert the values. It must be referentially transparent.
151 | */
152 | map(f : ((a : A) => B) | Lambda1) : Cell {
153 | const c = this;
154 | return Transaction.run(() =>
155 | Operational.updates(c).map(f).holdLazy(c.sampleLazy().map(Lambda1_toFunction(f)))
156 | );
157 | }
158 |
159 | /**
160 | * Lift a binary function into cells, so the returned Cell always reflects the specified
161 | * function applied to the input cells' values.
162 | * @param fn Function to apply. It must be referentially transparent.
163 | */
164 | lift(b : Cell,
165 | fn0 : ((a : A, b : B) => C) |
166 | Lambda2) : Cell
11 | * This is an OPERATIONAL primitive, which is not part of the main Sodium
12 | * API. It breaks the property of non-detectability of cell steps/updates.
13 | * The rule with this primitive is that you should only use it in functions
14 | * that do not allow the caller to detect the cell updates.
15 | */
16 | static updates(c : Cell) : Stream {
17 | /* Don't think this is needed
18 | const out = new StreamWithSend(null);
19 | out.setVertex__(new Vertex("updates", 0, [
20 | new Source(
21 | c.getStream__().getVertex__(),
22 | () => {
23 | return c.getStream__().listen_(out.getVertex__(), (a : A) => {
24 | out.send_(a);
25 | }, false);
26 | }
27 | ),
28 | new Source(
29 | c.getVertex__(),
30 | () => {
31 | return () => { };
32 | }
33 | )
34 | ]
35 | ));
36 | return out;
37 | */
38 | return c.getStream__();
39 | }
40 |
41 | /**
42 | * A stream that is guaranteed to fire once in the transaction where value() is invoked, giving
43 | * the current value of the cell, and thereafter behaves like {@link updates(Cell)},
44 | * firing for each update/step of the cell's value.
45 | *
46 | * This is an OPERATIONAL primitive, which is not part of the main Sodium
47 | * API. It breaks the property of non-detectability of cell steps/updates.
48 | * The rule with this primitive is that you should only use it in functions
49 | * that do not allow the caller to detect the cell updates.
50 | */
51 | static value(c : Cell) : Stream {
52 | return Transaction.run(() => {
53 | const sSpark = new StreamWithSend
89 | * In the case where two events are simultaneous (i.e. both
90 | * within the same transaction), the event from this will take precedence, and
91 | * the event from s will be dropped.
92 | * If you want to specify your own combining function, use {@link Stream#merge(Stream, Lambda2)}.
93 | * s1.orElse(s2) is equivalent to s1.merge(s2, (l, r) -> l).
94 | *
95 | * The name orElse() is used instead of merge() to make it really clear that care should
96 | * be taken, because events can be dropped.
97 | */
98 | orElse(s : Stream) : Stream {
99 | return this.merge(s, (left : A, right: A) => {
100 | return left;
101 | });
102 | }
103 |
104 | /**
105 | * Merge two streams of the same type into one, so that events on either input appear
106 | * on the returned stream.
107 | *
108 | * If the events are simultaneous (that is, one event from this and one from s
109 | * occurring in the same transaction), combine them into one using the specified combining function
110 | * so that the returned stream is guaranteed only ever to have one event per transaction.
111 | * The event from this will appear at the left input of the combining function, and
112 | * the event from s will appear at the right.
113 | * @param f Function to combine the values. It may construct FRP logic or use
114 | * {@link Cell#sample()}. Apart from this the function must be referentially transparent.
115 | */
116 | merge(s : Stream, f : ((left : A, right : A) => A) | Lambda2) : Stream {
117 | const ff = Lambda2_toFunction(f);
118 | const mergeState = new MergeState();
119 | let pumping = false;
120 | const out = new StreamWithSend(null);
121 | const pump = () => {
122 | if (pumping) {
123 | return;
124 | }
125 | pumping = true;
126 | Transaction.currentTransaction.prioritized(out.getVertex__(), () => {
127 | if (mergeState.left_present && mergeState.right_present) {
128 | out.send_(ff(mergeState.left, mergeState.right));
129 | } else if (mergeState.left_present) {
130 | out.send_(mergeState.left);
131 | } else if (mergeState.right_present) {
132 | out.send_(mergeState.right);
133 | }
134 | mergeState.left = null;
135 | mergeState.left_present = false;
136 | mergeState.right = null;
137 | mergeState.right_present = false;
138 | pumping = false;
139 | });
140 | };
141 | const vertex = new Vertex("merge", 0,
142 | [
143 | new Source(
144 | this.vertex,
145 | () => this.listen_(out.vertex, (a : A) => {
146 | mergeState.left = a;
147 | mergeState.left_present = true;
148 | pump();
149 | }, false)
150 | ),
151 | new Source(
152 | s.vertex,
153 | () => s.listen_(out.vertex, (a : A) => {
154 | mergeState.right = a;
155 | mergeState.right_present = true;
156 | pump();
157 | }, false)
158 | )
159 | ].concat(toSources(Lambda2_deps(f)))
160 | );
161 | out.vertex = vertex;
162 | return out;
163 | }
164 |
165 | /**
166 | * Return a stream that only outputs events for which the predicate returns true.
167 | */
168 | filter(f : ((a : A) => boolean) | Lambda1) : Stream {
169 | const out = new StreamWithSend(null);
170 | const ff = Lambda1_toFunction(f);
171 | out.vertex = new Vertex("filter", 0, [
172 | new Source(
173 | this.vertex,
174 | () => {
175 | return this.listen_(out.vertex, (a : A) => {
176 | if (ff(a))
177 | out.send_(a);
178 | }, false);
179 | }
180 | )
181 | ].concat(toSources(Lambda1_deps(f)))
182 | );
183 | return out;
184 | }
185 |
186 | /**
187 | * Return a stream that only outputs events that have present
188 | * values, discarding null values.
189 | */
190 | filterNotNull() : Stream {
191 | const out = new StreamWithSend(null);
192 | out.vertex = new Vertex("filterNotNull", 0, [
193 | new Source(
194 | this.vertex,
195 | () => {
196 | return this.listen_(out.vertex, (a : A) => {
197 | if (a !== null)
198 | out.send_(a);
199 | }, false);
200 | }
201 | )
202 | ]
203 | );
204 | return out;
205 | }
206 |
207 | /**
208 | * Return a stream that only outputs events from the input stream
209 | * when the specified cell's value is true.
210 | */
211 | gate(c : Cell
242 | * There is an implicit delay: State updates caused by event firings being held with
243 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until
244 | * the following transaction. To put this another way, {@link Stream#snapshot(Cell, Lambda2)}
245 | * always sees the value of a cell as it was before any state changes from the current
246 | * transaction.
247 | */
248 | snapshot(b : Cell, f_ : ((a : A, b : B) => C) | Lambda2) : Stream
271 | * There is an implicit delay: State updates caused by event firings being held with
272 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until
273 | * the following transaction. To put this another way, snapshot()
274 | * always sees the value of a cell as it was before any state changes from the current
275 | * transaction.
276 | */
277 | snapshot3(b : Cell, c : Cell
301 | * There is an implicit delay: State updates caused by event firings being held with
302 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until
303 | * the following transaction. To put this another way, snapshot()
304 | * always sees the value of a cell as it was before any state changes from the current
305 | * transaction.
306 | */
307 | snapshot4(b : Cell, c : Cell
334 | * There is an implicit delay: State updates caused by event firings being held with
335 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until
336 | * the following transaction. To put this another way, snapshot()
337 | * always sees the value of a cell as it was before any state changes from the current
338 | * transaction.
339 | */
340 | snapshot5(b : Cell, c : Cell
368 | * There is an implicit delay: State updates caused by event firings being held with
369 | * {@link Stream#hold(Object)} don't become visible as the cell's current value until
370 | * the following transaction. To put this another way, snapshot()
371 | * always sees the value of a cell as it was before any state changes from the current
372 | * transaction.
373 | */
374 | snapshot6(b : Cell, c : Cell
404 | * There is an implicit delay: State updates caused by event firings don't become
405 | * visible as the cell's current value as viewed by {@link Stream#snapshot(Cell, Lambda2)}
406 | * until the following transaction. To put this another way,
407 | * {@link Stream#snapshot(Cell, Lambda2)} always sees the value of a cell as it was before
408 | * any state changes from the current transaction.
409 | */
410 | hold(initValue : A) : Cell {
411 | return new Cell(initValue, this);
412 | }
413 |
414 | /**
415 | * A variant of {@link hold(Object)} with an initial value captured by {@link Cell#sampleLazy()}.
416 | */
417 | holdLazy(initValue : Lazy) : Cell {
418 | return new LazyCell(initValue, this);
419 | }
420 |
421 | /**
422 | * Transform an event with a generalized state loop (a Mealy machine). The function
423 | * is passed the input and the old state and returns the new state and output value.
424 | * @param f Function to apply to update the state. It may construct FRP logic or use
425 | * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the
426 | * cell. Apart from this the function must be referentially transparent.
427 | */
428 | collect(initState : S, f : ((a : A, s : S) => Tuple2) | Lambda2>) : Stream {
429 | return this.collectLazy(new Lazy(() => { return initState; }), f);
430 | }
431 |
432 | /**
433 | * A variant of {@link collect(Object, Lambda2)} that takes an initial state returned by
434 | * {@link Cell#sampleLazy()}.
435 | */
436 | collectLazy(initState : Lazy, f : ((a : A, s : S) => Tuple2) | Lambda2>) : Stream {
437 | const ea = this;
438 | return Transaction.run(() => {
439 | const es = new StreamLoop(),
440 | s = es.holdLazy(initState),
441 | ebs = ea.snapshot(s, f),
442 | eb = ebs.map((bs : Tuple2) => { return bs.a; }),
443 | es_out = ebs.map((bs : Tuple2) => { return bs.b; });
444 | es.loop(es_out);
445 | return eb;
446 | });
447 | }
448 |
449 | /**
450 | * Accumulate on input event, outputting the new state each time.
451 | * @param f Function to apply to update the state. It may construct FRP logic or use
452 | * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the
453 | * cell. Apart from this the function must be referentially transparent.
454 | */
455 | accum(initState : S, f : ((a : A, s : S) => S) | Lambda2) : Cell {
456 | return this.accumLazy(new Lazy(() => { return initState; }), f);
457 | }
458 |
459 | /**
460 | * A variant of {@link accum(Object, Lambda2)} that takes an initial state returned by
461 | * {@link Cell#sampleLazy()}.
462 | */
463 | accumLazy(initState : Lazy, f : ((a : A, s : S) => S) | Lambda2) : Cell {
464 | const ea = this;
465 | return Transaction.run(() => {
466 | const es = new StreamLoop(),
467 | s = es.holdLazy(initState),
468 | es_out = ea.snapshot(s, f);
469 | es.loop(es_out);
470 | return es_out.holdLazy(initState);
471 | });
472 | }
473 |
474 | /**
475 | * Return a stream that outputs only one value: the next event of the
476 | * input stream, starting from the transaction in which once() was invoked.
477 | */
478 | once() : Stream {
479 | /*
480 | return Transaction.run(() => {
481 | const ev = this,
482 | out = new StreamWithSend();
483 | let la : () => void = null;
484 | la = ev.listen_(out.vertex, (a : A) => {
485 | if (la !== null) {
486 | out.send_(a);
487 | la();
488 | la = null;
489 | }
490 | }, false);
491 | return out;
492 | });
493 | */
494 | // We can't use the implementation above, because unregistering
495 | // listeners triggers the exception
496 | // "send() was invoked before listeners were registered"
497 | // We can revisit this another time. For now we will use the less
498 | // efficient implementation below.
499 | const me = this;
500 | return Transaction.run(() => me.gate(me.mapTo(false).hold(true)));
501 | }
502 |
503 | listen(h : (a : A) => void) : () => void {
504 | return Transaction.run<() => void>(() => {
505 | return this.listen_(Vertex.NULL, h, false);
506 | });
507 | }
508 |
509 | listen_(target : Vertex,
510 | h : (a : A) => void,
511 | suppressEarlierFirings : boolean) : () => void {
512 | if (this.vertex.register(target))
513 | Transaction.currentTransaction.requestRegen();
514 | const listener = new Listener(h, target);
515 | this.listeners.push(listener);
516 | if (!suppressEarlierFirings && this.firings.length != 0) {
517 | const firings = this.firings.slice();
518 | Transaction.currentTransaction.prioritized(target, () => {
519 | // Anything sent already in this transaction must be sent now so that
520 | // there's no order dependency between send and listen.
521 | for (let i = 0; i < firings.length; i++)
522 | h(firings[i]);
523 | });
524 | }
525 | return () => {
526 | let removed = false;
527 | for (let i = 0; i < this.listeners.length; i++) {
528 | if (this.listeners[i] == listener) {
529 | this.listeners.splice(i, 1);
530 | removed = true;
531 | break;
532 | }
533 | }
534 | if (removed)
535 | this.vertex.deregister(target);
536 | };
537 | }
538 |
539 |
540 | /**
541 | * Fantasy-land Algebraic Data Type Compatibility.
542 | * Stream satisfies the Functor and Monoid Categories (and hence Semigroup)
543 | * @see {@link https://github.com/fantasyland/fantasy-land} for more info
544 | */
545 |
546 | //map :: Functor f => f a ~> (a -> b) -> f b
547 | 'fantasy-land/map'(f : ((a : A) => B)) : Stream {
548 | return this.map(f);
549 | }
550 |
551 | //concat :: Semigroup a => a ~> a -> a
552 | 'fantasy-land/concat'(a:Stream) : Stream {
553 | return this.merge(a, (left:any, right) => {
554 | return (Z.Semigroup.test(left)) ? Z.concat(left, right) : left;
555 | });
556 | }
557 |
558 | //empty :: Monoid m => () -> m
559 | 'fantasy-land/empty'() : Stream {
560 | return new Stream();
561 | }
562 | }
563 |
564 | export class StreamWithSend extends Stream {
565 | constructor(vertex? : Vertex) {
566 | super(vertex);
567 | }
568 |
569 | setVertex__(vertex : Vertex) { // TO DO figure out how to hide this
570 | this.vertex = vertex;
571 | }
572 |
573 | send_(a : A) : void {
574 | if (this.firings.length == 0)
575 | Transaction.currentTransaction.last(() => {
576 | this.firings = [];
577 | });
578 | this.firings.push(a);
579 | const listeners = this.listeners.slice();
580 | for (let i = 0; i < listeners.length; i++) {
581 | const h = listeners[i].h;
582 | Transaction.currentTransaction.prioritized(listeners[i].target, () => {
583 | Transaction.currentTransaction.inCallback++;
584 | try {
585 | h(a);
586 | Transaction.currentTransaction.inCallback--;
587 | }
588 | catch (err) {
589 | Transaction.currentTransaction.inCallback--;
590 | throw err;
591 | }
592 | });
593 | }
594 | }
595 | }
596 |
597 | /**
598 | * A forward reference for a {@link Stream} equivalent to the Stream that is referenced.
599 | */
600 | export class StreamLoop extends StreamWithSend {
601 | assigned__ : boolean = false; // to do: Figure out how to hide this
602 |
603 | constructor()
604 | {
605 | super();
606 | this.vertex.name = "StreamLoop";
607 | if (Transaction.currentTransaction === null)
608 | throw new Error("StreamLoop/CellLoop must be used within an explicit transaction");
609 | }
610 |
611 | /**
612 | * Resolve the loop to specify what the StreamLoop was a forward reference to. It
613 | * must be invoked inside the same transaction as the place where the StreamLoop is used.
614 | * This requires you to create an explicit transaction with {@link Transaction#run(Lambda0)}
615 | * or {@link Transaction#runVoid(Runnable)}.
616 | */
617 | loop(sa_out : Stream) : void {
618 | if (this.assigned__)
619 | throw new Error("StreamLoop looped more than once");
620 | this.assigned__ = true;
621 | this.vertex.addSource(
622 | new Source(
623 | sa_out.getVertex__(),
624 | () => {
625 | return sa_out.listen_(this.vertex, (a : A) => {
626 | this.send_(a);
627 | }, false);
628 | }
629 | )
630 | );
631 | }
632 | }
633 |
--------------------------------------------------------------------------------
/src/lib/sodium/StreamSink.ts:
--------------------------------------------------------------------------------
1 | import { Lambda1, Lambda1_deps, Lambda1_toFunction,
2 | Lambda2, Lambda2_deps, Lambda2_toFunction } from "./Lambda";
3 | import { StreamWithSend } from "./Stream";
4 | import { CoalesceHandler } from "./CoalesceHandler";
5 | import { Transaction } from "./Transaction";
6 | import { Vertex } from './Vertex';
7 |
8 | /**
9 | * A stream that allows values to be pushed into it, acting as an interface between the
10 | * world of I/O and the world of FRP. Code that exports StreamSinks for read-only use
11 | * should downcast to {@link Stream}.
12 | */
13 | export class StreamSink extends StreamWithSend {
14 | private disableListenCheck: boolean = false;
15 |
16 | constructor(f? : ((l : A, r : A) => A) | Lambda2) {
17 | super();
18 | if (!f)
19 | f = <(l : A, r : A) => A>((l : A, r : A) => {
20 | throw new Error("send() called more than once per transaction, which isn't allowed. Did you want to combine the events? Then pass a combining function to your StreamSink constructor.");
21 | });
22 | this.coalescer = new CoalesceHandler(f, this);
23 | }
24 |
25 | private coalescer : CoalesceHandler;
26 |
27 | send(a : A) : void {
28 | Transaction.run