NOTE Because Rx doesn’t have transactions, this section doesn’t apply to it.
Let’s say you have data packets, each of which contains several commands, and you want to feed them into the FRP logic (see figure 8.1). Each packet is processed in a separate transaction, so in this diagram there are two transactions.
To write this, the logic would need to deal throughout with lists of commands.
That would be a bit clunky. What you’d really like to do is split these commands out and process each one in its own transaction, as in figure 8.2.
split
split A
F Packet 1
FRP logic Packet 2
cmd1 cmd2 cmd3 cmd4 cmd5 B C
D E
G
H I
J K L
Figure 8.2 Let’s split the packets into individual commands.
cmd1 cmd2 cmd3
cmd4 Packet 1
FRP logic
cmd4 Packet 2
Figure 8.1 Packets come into the system, each containing a list of commands.
181 Spawning new transactional contexts with the split primitive
On the input side you have two transactions, but split lets you to turn that into five transactions. The original transactions are still there, so you get these seven transactions:
1 Packet 1
2 cmd1
3 cmd2
4 cmd3
5 Packet 2
6 cmd4
7 cmd5
The following listing shows some code that demonstrates this. Instead of executing commands, it adds numbers together.
NOTE Recall that accum() is a helper that gives you an accumulator. It’s shorthand for a simple hold-snapshot loop.
import nz.sodium.*;
import java.util.List;
import java.util.Arrays;
public class split {
public static void main(String[] args) {
StreamSink<List<Integer>> as = new StreamSink<>();
Listener l = Operational.updates(
Operational.split(as)
.<Integer>accum(0, (a, b) -> a + b) ).listen(total -> { System.out.println(total); });
as.send(Arrays.asList(100, 15, 60));
as.send(Arrays.asList(1, 5));
l.unlisten();
} }
ant split split:
[java] 100 [java] 115 [java] 175 [java] 176 [java] 181
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 -Psplit or ant split
Listing 8.6 Splitting list elements into their own transactions
You can nest split, too. If you have a list of a list and split it twice, then each sub- element ends up in its own transaction;
see figure 8.3.
8.5.1 Deferring a single event to a new transaction
If you want to put a single event into a new transaction, this is called deferring the event. It’s implemented as a variant of split that works on a single event instead of a list. The Operational.defer()
method provides that in the Java version of Sodium. This primitive gives you a way to read the new version of a cell’s state.
Here’s a use case for defer. Imagine you’re writing the logic for a communication protocol, and you receive some sort of timeout event. You want to turn that into a retry event. Let’s say the timeout sets an idle flag to true, indicating that a message is allowed to be sent. When the retry happens, you want it to see the idle flag after it’s been set. defer allows you to do this.
Here’s a sketch of the code. We briefly mentioned gate() in chapter 4—it’s a helper based on snapshot and filter that lets events through when the Boolean cell you pass it is true:
Cell<Message> msg = ...;
Stream<Unit> sInitiate = ...;
StreamLoop<Unit> sRetry = new StreamLoop<>();
CellLoop<Boolean> idle = new CellLoop<>();
Stream<Message> sSend = sInitiate.merge(sRetry).snapshot(msg) .gate(idle);
Stream<Unit> sTimeout = ...;
idle.loop(sSend.map(u -> false).merge(
sTimeout.map(u -> true)).hold(true));
sRetry.loop(Operational.defer(sTimeout));
It’s a bit unrealistic as it is, but it’s loosely based on a real-world case where this issue came up.
Sodium has an implicit delay
The problem in this example is a by-product of the fact that Sodium delays state updates until after the transaction has completed. In chapter 2, we said that you could alternatively view this delay as a separate primitive. Seen this way, Sodium implicitly adds delay to hold. Some systems (such as Yampa) do indeed have a separate delay primitive (dHold being equivalent to hold followed by the delay primitive iPre). In a strange way, therefore, defer could be seen as something that reverses delay.
A
F Packet 1
Packet 2 B C
D E
G
H I
J K L
A B C
D E
F G
H I
J K L
H I A B C D E F G
J K L
split split
Figure 8.3 Split twice to flatten a list of lists.
Blocks send if
not idle Defers the retry event so
idle is true and the gate doesn’t block the retry
183 Scalable addressing
The previous snippet also serves to illustrate why FRP feels like wooden teeth when you try to express sequences in it. You can write this in FRP, but a threaded style is more natural, like in this rough pseudo-code:
while (retryCount < 3) { send(msg);
reply = timeout(1000, recv());
if (reply != TIMEOUT) break;
retryCount++;
}
FRP is a great hammer, but not every problem is a nail.
8.5.2 Ending up in the same transaction
When defer or split is used in more than one place in the program, the output events can end up in the same transaction:
StreamSink<String> in = new StreamSink<>();
Stream<String> lower = in.map(x -> x.toLowerCase());
Stream<String> both = Operational.defer(in)
.orElse(Operational.defer(lower));
List<String> out = new ArrayList();
both.listen(x -> out.add(x));
sink.send("A");
sink.send("B");
sink.send("C");
System.out.println(out);
This code outputs [A, B, C] because two defers put the events into the same new transaction and orElse() gives precedence to in (the capital letters). Why didn’t we just make Sodium put them into different new transactions? The denotational seman- tics make you do it this way because there’s no compositional way to decide what order they should be in.
This is why we put defer() and split() into the Operational sin bin. They’re potentially useful, but be aware that using defer() more than once in a program can generate simultaneous events.