For the entire book, you’ve been saying, “Just tell me already!” and we’ve been pre- tending to be Yoda from Star Wars, saying “Ready you are not! Hmmm?” Now we judge that your mind is sufficiently purified that you can learn how to send data into FRP logic and receive it out without your FRP programming being influenced by opera- tional thinking. We’re about to teach you something that is necessary but that you should do only when necessary.
NOTE What follows is how Sodium does it, which should be fairly typical, but there will be variations in different FRP systems. For information about how to do this in RxJS, see “Creating your own hot observable” in section 6.5.
8.1.1 Sending and listening to streams
Interfacing your FRP code to the rest of your program has two parts:
1 Push events into streams or cells.
2 Listen to the events from streams or cells.
Listing 8.1 shows both of these. You construct a StreamSink, which is a subclass of Stream that adds a method called send(), allowing you to push or send values into the stream.
NOTE With certain rules on listeners to be discussed shortly, it’s absolutely thread-safe and safe in every other way to call send() from any context. The send() call will also never block on I/O.
When you export a StreamSink from a module for other parts of the program to con- sume the stream, you should upcast it to Stream before doing so, so the ability to push events into the stream isn’t made public.
NOTE Your code can do all the I/O and state mutation it wants in between calls to send(). We’re now in the Wild West where the strict rules of referen- tial transparency don’t apply.
To listen to a stream’s events, you can register a listener on any Stream with the listen() method. It returns a Listener object that has an unlisten() method to deregister the listener when you’ve finished observing the values from the stream:
Listener l = ev.listen(value -> { ... do something ... ; });
...
l.unlisten();
You can also append() listeners together into one, so a common idiom to simplify unlistening is this:
Listener l = new Listener();
l = l.append(sX.listen(...));
l = l.append(sY.listen(...));
...
l.unlisten();
Deregisters all callbacks
171 Interfacing FRP code with the rest of your program
The following are Sodium-specific:
■ The stuff that Sodium does behind the scenes is all automatic, but deregistering these explicit listeners is not automatic. If you forget to do so, the FRP logic is held in memory, and this could result in a memory leak. But FRP programs typi- cally don’t have many listeners to be concerned about, and often they exist for the life of the program anyway.
■ When you listen to something with listen(), all the associated FRP logic is held in memory and isn’t garbage-collected until you explicitly call unlisten(). There is a variant of listen() called listenWeak() that automatically deregis- ters the handler if the Listener object is garbage-collected.
import nz.sodium.*;
public class stream {
public static void main(String[] args) {
StreamSink<Integer> sX = new StreamSink<>();
Stream<Integer> sXPlus1 = sX.map(x -> x + 1);
Listener l = sXPlus1.listen(x -> { System.out.println(x); });
sX.send(1);
sX.send(2);
sX.send(3);
l.unlisten();
} }
ant stream stream:
[java] 2 [java] 3 [java] 4
To run this, check it out if you haven’t done so already, and then run it like this:
git clone https://github.com/SodiumFRP/sodium cd sodium/book/operational/java
mvn test -Pstream or ant stream
8.1.2 Multiple send()s in a single transaction
NOTE This section doesn’t apply to Rx and other systems that lack a concept of transactions.
When you create an event by issuing a send(), it automatically starts a transaction within which all the state changes that result from it in your FRP logic take place. As we’ve mentioned before and will cover in more detail shortly, it’s possible to start transactions explicitly. If you call send() on more than one StreamSink in a single transaction, the resulting events are simultaneous with each other.
But what if you send to the same StreamSink more than once? Sodium only allows one event per transaction on a given stream, so how does it deal with this situation? By default, it throws away the events from all but the last send().
Listing 8.1 Sending data into and listening to a stream
If you want to do something different, you can use a variant of StreamSink’s con- structor that takes a combining function. For example, if you’re using the type Stream- Sink<Integer> and you want to add the numbers when combining, you can do this:
StreamSink<Integer> s = new StreamSink<>((a, b) -> a + b);
...
s.send(1);
Transaction.runVoid(() -> { s.send(5);
s.send(7);
});
s.send(100);
In this explicit transaction, the numbers are added together using the function speci- fied giving a single event with the value 12. The events you’ll see on s in this program are as follows:
1 12 100
8.1.3 Sending and listening to cells
Listing 8.2 constructs a CellSink. Because a cell must always have a value, it requires you to specify an initial value. Cell.listen() is the same as its stream counterpart, except that you’re called back once on registration of the listener with the cell’s cur- rent value. Note that the output shows the initial 0.
import nz.sodium.*;
public class cell {
public static void main(String[] args) { CellSink<Integer> x = new CellSink<>(0);
Listener l = x.listen(x_ -> { System.out.println(x_); });
x.send(10);
x.send(20);
x.send(30);
l.unlisten();
} }
ant cell cell:
[java] 0 [java] 10 [java] 20 [java] 30
To run this, check it out if you haven’t done so already, and then run it like this:
cd sodium/book/operational/java mvn test -Pcell or ant cell
Listing 8.2 Sending data into and listening to a cell
Groups the two sends into a single transaction
173 Interfacing FRP code with the rest of your program
8.1.4 Threading model and callback requirements
To make things work consistently, there are certain rules about what you can and can’t do in a listen() callback. These rules should be considered a requirement in Sodium, but they’re good advice in any FRP system.
Sodium currently works in the following way, but there’s a good chance this could change in the future for performance reasons:
■ In Sodium, you’re called back on the thread that the send() that started the FRP processing was called on.
Other systems may behave differently. But no matter what FRP system you’re using, you’ll avoid many problems if you don’t make assumptions about what thread you were called back on. The rules we give in this chapter are based on this idea.
Having said that, if you carefully inspect the implementation of the SWidgets library used in the examples, you’ll see that we’re using such assumptions to obtain specific behavior: we’re trying to make Swing comply with some FRP-like assumptions about when state changes will occur. Sometimes this is the right thing to do. This will generally be when you’re trying to wrap some non-FRP logic with an FRP interface, as in this case.
Without further ado, here are the rules you should use for listen() callbacks:
■ You’re not allowed to send() inside a callback. In Sodium, doing so throws an exception. There are two reasons. First, we don’t encourage this style, and sec- ond, we can’t maintain correct processing order. If you need to write your own primitive, this means we should improve Sodium. This may need to happen from time to time.
■ You’re not allowed to block inside a callback.
■ Nonblocking I/O is acceptable.
The way to do blocking I/O is to delegate your processing to a worker thread. At the conclusion of the I/O, you can send() a result back into your FRP logic. Because the worker thread wouldn’t be blocking the callback, this is valid.
The rules of interfacing with FRP in Sodium
We can summarize the rules of interfacing your I/O code with FRP like this:
■ It’s safe to call send() from any context except an FRP listener, and it never blocks.
■ The functions passed to FRP logic can do anything that’s referentially transpar- ent, and functions that work with stream events may construct FRP logic and may use sample. I/O is forbidden.
■ Listeners must not block or call send(), and they may do nonblocking I/O, including delegating work to other threads.
If these rules are followed, you’ll never have threading issues. This is true in Sodium and may be true in other FRP systems.