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();
}
}
| |