All Downloads are FREE. Search and download functionalities are using the official Maven repository.

nz.sodium.Stream Maven / Gradle / Ivy

package nz.sodium;

import java.util.ArrayList;
import java.util.List;
import java.util.HashSet;
import java.util.Optional;
import java.util.Vector;

/**
 * Represents a stream of discrete events/firings containing values of type A. 
 */
public class Stream {
	private static final class ListenerImplementation extends Listener {
		/**
		 * It's essential that we keep the listener alive while the caller holds
		 * the Listener, so that the finalizer doesn't get triggered.
		 */
		private Stream event;
		/**
		 * It's also essential that we keep the action alive, since the node uses
		 * a weak reference.
		 */
		private TransactionHandler action;
		private Node.Target target;

		private ListenerImplementation(Stream event, TransactionHandler action, Node.Target target) {
			this.event = event;
			this.action = action;
			this.target = target;
		}

		public void unlisten() {
		    synchronized (Transaction.listenersLock) {
		        if (this.event != null) {
                    event.node.unlinkTo(target);
                    this.event = null;
                    this.action = null;
                    this.target = null;
                }
            }
		}
	}

	final Node node;
	final List finalizers;
	final List firings;

	/**
	 * A stream that never fires.
	 */
	public Stream() {
	    this.node = new Node(0L);
	    this.finalizers = new ArrayList();
	    this.firings = new ArrayList();
	}

	private Stream(Node node, List finalizers, List firings) {
	    this.node = node;
	    this.finalizers = finalizers;
        this.firings = firings;
	}

    static HashSet keepListenersAlive = new HashSet();

	/**
	 * Listen for events/firings on this stream. 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 handler 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.
     */
	public final Listener listen(final Handler handler) {
        final Listener l0 = listenWeak(handler);
        Listener l = new Listener() {
            public void unlisten() {
                l0.unlisten();
                synchronized (keepListenersAlive) {
                    keepListenersAlive.remove(this);
                }
            }
        };
        synchronized (keepListenersAlive) {
            keepListenersAlive.add(l);
        }
        return l;
	}

    /**
     * A variant of {@link listen(Handler)} that handles the first event and then
     * automatically deregisters itself. This is useful for implementing things that
     * work like promises.
     */
    public final Listener listenOnce(final Handler handler) {
        final Listener[] lRef = new Listener[1];
        lRef[0] = listen(new Handler() {
            public void run(A a) {
                lRef[0].unlisten();
                handler.run(a);
            }
        });
        return lRef[0];
    }

	final Listener listen_(final Node target, final TransactionHandler action) {
		return Transaction.apply(new Lambda1() {
			public Listener apply(Transaction trans1) {
				return listen(target, trans1, action, false);
			}
		});
	}

    /**
     * A variant of {@link listen(Handler)} that will deregister the listener automatically
     * if the listener is garbage collected. With {@link listen(Handler)}, the listener is
     * only deregistered if {@link Listener#unlisten()} is called explicitly.
     * 

* This method should be used for listeners that are to be passed to {@link Stream#addCleanup(Listener)} * to ensure that things don't get kept alive when they shouldn't. */ public final Listener listenWeak(final Handler action) { return listen_(Node.NULL, new TransactionHandler() { public void run(Transaction trans2, A a) { action.run(a); } }); } @SuppressWarnings("unchecked") final Listener listen(Node target, Transaction trans, final TransactionHandler action, boolean suppressEarlierFirings) { Node.Target[] node_target_ = new Node.Target[1]; synchronized (Transaction.listenersLock) { if (node.linkTo((TransactionHandler)action, target, node_target_)) trans.toRegen = true; } Node.Target node_target = node_target_[0]; final List firings = new ArrayList(this.firings); if (!suppressEarlierFirings && !firings.isEmpty()) trans.prioritized(target, new Handler() { public void run(Transaction trans2) { // Anything sent already in this transaction must be sent now so that // there's no order dependency between send and listen. for (A a : firings) { Transaction.inCallback++; try { // Don't allow transactions to interfere with Sodium // internals. action.run(trans2, a); } catch (Throwable t) { t.printStackTrace(); } finally { Transaction.inCallback--; } } } }); return new ListenerImplementation(this, action, node_target); } /** * Transform the stream's event values according to the supplied function, so the returned * Stream's event values reflect the value of the function applied to the input * Stream's event values. * @param f Function to apply to convert the values. It may construct FRP logic or use * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the * cell. Apart from this the function must be referentially transparent. */ public final Stream map(final Lambda1 f) { final Stream ev = this; final StreamWithSend out = new StreamWithSend(); Listener l = listen_(out.node, new TransactionHandler() { public void run(Transaction trans2, A a) { out.send(trans2, f.apply(a)); } }); return out.unsafeAddCleanup(l); } /** * Transform the stream's event values into the specified constant value. * @param b Constant value. */ public final Stream mapTo(final B b) { return this.map(new Lambda1() { public B apply(A a) { return b; } }); } /** * Create a {@link Cell} with the specified initial value, that is updated * by this stream's event values. *

* There is an implicit delay: State updates caused by event firings don't become * visible as the cell's current value as viewed by {@link Stream#snapshot(Cell, Lambda2)} * until the following transaction. To put this another way, * {@link Stream#snapshot(Cell, Lambda2)} always sees the value of a cell as it was before * any state changes from the current transaction. */ public final Cell hold(final A initValue) { return Transaction.apply(new Lambda1>() { public Cell apply(Transaction trans) { return new Cell(Stream.this, initValue); } }); } /** * A variant of {@link hold(Object)} with an initial value captured by {@link Cell#sampleLazy()}. */ public final Cell holdLazy(final Lazy initValue) { return Transaction.apply(new Lambda1>() { public Cell apply(Transaction trans) { return holdLazy(trans, initValue); } }); } final Cell holdLazy(Transaction trans, final Lazy initValue) { return new LazyCell(this, initValue); } /** * Variant of {@link snapshot(Cell, Lambda2)} that captures the cell's value * at the time of the event firing, ignoring the stream's value. */ public final Stream snapshot(Cell c) { return snapshot(c, new Lambda2() { public B apply(A a, B b) { return b; } }); } /** * Return a stream whose events are the result of the combination using the specified * function of the input stream's event value and the value of the cell at that time. *

* There is an implicit delay: State updates caused by event firings being held with * {@link Stream#hold(Object)} don't become visible as the cell's current value until * the following transaction. To put this another way, {@link Stream#snapshot(Cell, Lambda2)} * always sees the value of a cell as it was before any state changes from the current * transaction. */ public final Stream snapshot(final Cell c, final Lambda2 f) { final Stream ev = this; final StreamWithSend out = new StreamWithSend(); Listener l = listen_(out.node, new TransactionHandler() { public void run(Transaction trans2, A a) { out.send(trans2, f.apply(a, c.sampleNoTrans())); } }); return out.unsafeAddCleanup(l); } /** * Variant of {@link snapshot(Cell, Lambda2)} that captures the values of * two cells. */ public final Stream snapshot(final Cell cb, final Cell cc, final Lambda3 fn) { return this.snapshot(cb, new Lambda2() { public D apply(A a, B b) { return fn.apply(a, b, cc.sample()); } }); } /** * Variant of {@link snapshot(Cell, Lambda2)} that captures the values of * three cells. */ public final Stream snapshot(final Cell cb, final Cell cc, final Cell cd, final Lambda4 fn) { return this.snapshot(cb, new Lambda2() { public E apply(A a, B b) { return fn.apply(a, b, cc.sample(), cd.sample()); } }); } /** * Variant of {@link snapshot(Cell, Lambda2)} that captures the values of * four cells. */ public final Stream snapshot(final Cell cb, final Cell cc, final Cell cd, final Cell ce, final Lambda5 fn) { return this.snapshot(cb, new Lambda2() { public F apply(A a, B b) { return fn.apply(a, b, cc.sample(), cd.sample(), ce.sample()); } }); } /** * Variant of {@link snapshot(Cell, Lambda2)} that captures the values of * five cells. */ public final Stream snapshot(final Cell cb, final Cell cc, final Cell cd, final Cell ce, final Cell cf, final Lambda6 fn) { return this.snapshot(cb, new Lambda2() { public G apply(A a, B b) { return fn.apply(a, b, cc.sample(), cd.sample(), ce.sample(), cf.sample()); } }); } /** * Variant of {@link Stream#merge(Stream, Lambda2)} that merges two streams and will drop an event * in the simultaneous case. *

* In the case where two events are simultaneous (i.e. both * within the same transaction), the event from this will take precedence, and * the event from s will be dropped. * If you want to specify your own combining function, use {@link Stream#merge(Stream, Lambda2)}. * s1.orElse(s2) is equivalent to s1.merge(s2, (l, r) -> l). *

* The name orElse() is used instead of merge() to make it really clear that care should * be taken, because events can be dropped. */ public final Stream orElse(final Stream s) { return merge(s, new Lambda2() { public A apply(A left, A right) { return left; } }); } private static Stream merge(final Stream ea, final Stream eb) { final StreamWithSend out = new StreamWithSend(); final Node left = new Node(0); final Node right = out.node; Node.Target[] node_target_ = new Node.Target[1]; left.linkTo(null, right, node_target_); final Node.Target node_target = node_target_[0]; TransactionHandler h = new TransactionHandler() { public void run(Transaction trans, A a) { out.send(trans, a); } }; Listener l1 = ea.listen_(left, h); Listener l2 = eb.listen_(right, h); return out.unsafeAddCleanup(l1).unsafeAddCleanup(l2).unsafeAddCleanup(new Listener() { public void unlisten() { left.unlinkTo(node_target); } }); } /** * Merge two streams of the same type into one, so that events on either input appear * on the returned stream. *

* If the events are simultaneous (that is, one event from this and one from s * occurring in the same transaction), combine them into one using the specified combining function * so that the returned stream is guaranteed only ever to have one event per transaction. * The event from this will appear at the left input of the combining function, and * the event from s will appear at the right. * @param f Function to combine the values. It may construct FRP logic or use * {@link Cell#sample()}. Apart from this the function must be referentially transparent. */ public final Stream merge(final Stream s, final Lambda2 f) { return Transaction.apply(new Lambda1>() { public Stream apply(Transaction trans) { return Stream.merge(Stream.this, s).coalesce(trans, f); } }); } /** * Variant of {@link orElse(Stream)} that merges a collection of streams. */ public static Stream orElse(Iterable> ss) { return Stream.merge(ss, new Lambda2() { public A apply(A left, A right) { return right; } }); } /** * Variant of {@link merge(Stream,Lambda2)} that merges a collection of streams. */ public static Stream merge(Iterable> ss, final Lambda2 f) { Vector> v = new Vector>(); for (Stream s : ss) v.add(s); return merge(v, 0, v.size(), f); } private static Stream merge(Vector> sas, int start, int end, final Lambda2 f) { int len = end - start; if (len == 0) return new Stream(); else if (len == 1) return sas.get(start); else if (len == 2) return sas.get(start).merge(sas.get(start+1), f); else { int mid = (start + end) / 2; return Stream.merge(sas, start, mid, f).merge(Stream.merge(sas, mid, end, f), f); } } private final Stream coalesce(Transaction trans1, final Lambda2 f) { final Stream ev = this; final StreamWithSend out = new StreamWithSend(); TransactionHandler h = new CoalesceHandler(f, out); Listener l = listen(out.node, trans1, h, false); return out.unsafeAddCleanup(l); } /** * Clean up the output by discarding any firing other than the last one. */ final Stream lastFiringOnly(Transaction trans) { return coalesce(trans, new Lambda2() { public A apply(A first, A second) { return second; } }); } /** * Return a stream that only outputs events for which the predicate returns true. */ public final Stream filter(final Lambda1 predicate) { final Stream ev = this; final StreamWithSend out = new StreamWithSend(); Listener l = listen_(out.node, new TransactionHandler() { public void run(Transaction trans2, A a) { if (predicate.apply(a)) out.send(trans2, a); } }); return out.unsafeAddCleanup(l); } /** * Return a stream that only outputs events that have present * values, removing the {@link java.util.Optional} wrapper, discarding empty values. */ public static Stream filterOptional(final Stream> ev) { final StreamWithSend out = new StreamWithSend(); Listener l = ev.listen_(out.node, new TransactionHandler>() { public void run(Transaction trans2, Optional oa) { if (oa.isPresent()) out.send(trans2, oa.get()); } }); return out.unsafeAddCleanup(l); } /** * Return a stream that only outputs events from the input stream * when the specified cell's value is true. */ public final Stream gate(Cell c) { return Stream.filterOptional( snapshot(c, new Lambda2>() { public Optional apply(A a, Boolean pred) { return pred ? Optional.of(a) : Optional.empty(); } }) ); } /** * Transform an event with a generalized state loop (a Mealy machine). The function * is passed the input and the old state and returns the new state and output value. * @param f Function to apply to update the state. It may construct FRP logic or use * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the * cell. Apart from this the function must be referentially transparent. */ public final Stream collect(final S initState, final Lambda2> f) { return collectLazy(new Lazy(initState), f); } /** * A variant of {@link collect(Object, Lambda2)} that takes an initial state returned by * {@link Cell#sampleLazy()}. */ public final Stream collectLazy(final Lazy initState, final Lambda2> f) { return Transaction.>run(new Lambda0>() { public Stream apply() { final Stream ea = Stream.this; StreamLoop es = new StreamLoop(); Cell s = es.holdLazy(initState); Stream> ebs = ea.snapshot(s, f); Stream eb = ebs.map(new Lambda1,B>() { public B apply(Tuple2 bs) { return bs.a; } }); Stream es_out = ebs.map(new Lambda1,S>() { public S apply(Tuple2 bs) { return bs.b; } }); es.loop(es_out); return eb; } }); } /** * Accumulate on input event, outputting the new state each time. * @param f Function to apply to update the state. It may construct FRP logic or use * {@link Cell#sample()} in which case it is equivalent to {@link Stream#snapshot(Cell)}ing the * cell. Apart from this the function must be referentially transparent. */ public final Cell accum(final S initState, final Lambda2 f) { return accumLazy(new Lazy(initState), f); } /** * A variant of {@link accum(Object, Lambda2)} that takes an initial state returned by * {@link Cell#sampleLazy()}. */ public final Cell accumLazy(final Lazy initState, final Lambda2 f) { return Transaction.>run(new Lambda0>() { public Cell apply() { final Stream ea = Stream.this; StreamLoop es = new StreamLoop(); Cell s = es.holdLazy(initState); Stream es_out = ea.snapshot(s, f); es.loop(es_out); return es_out.holdLazy(initState); } }); } /** * Return a stream that outputs only one value: the next event of the * input stream, starting from the transaction in which once() was invoked. */ public final Stream once() { // This is a bit long-winded but it's efficient because it deregisters // the listener. final Stream ev = this; final Listener[] la = new Listener[1]; final StreamWithSend out = new StreamWithSend(); la[0] = ev.listen_(out.node, new TransactionHandler() { public void run(Transaction trans, A a) { if (la[0] != null) { out.send(trans, a); la[0].unlisten(); la[0] = null; } } }); return out.unsafeAddCleanup(la[0]); } /** * This is not thread-safe, so one of these two conditions must apply: * 1. We are within a transaction, since in the current implementation * a transaction locks out all other threads. * 2. The object on which this is being called was created has not yet * been returned from the method where it was created, so it can't * be shared between threads. */ Stream unsafeAddCleanup(Listener cleanup) { finalizers.add(cleanup); return this; } /** * Attach a listener to this stream so that its {@link Listener#unlisten()} is invoked * when this stream is garbage collected. Useful for functions that initiate I/O, * returning the result of it through a stream. *

* You must use this only with listeners returned by {@link listenWeak(Handler)} so that * things don't get kept alive when they shouldn't. */ public Stream addCleanup(final Listener cleanup) { return Transaction.run(new Lambda0>() { public Stream apply() { List fsNew = new ArrayList(finalizers); fsNew.add(cleanup); return new Stream(node, fsNew, firings); } }); } @Override protected void finalize() throws Throwable { for (Listener l : finalizers) l.unlisten(); } }