Let’s say you like playing Japanese board games online. The game window has a little chat window next to it. Let’s add a couple of buttons for canned messages to make it more convenient to say two commonly used phrases: “Onegai shimasu” to greet your opponent, and “Thank you” at the end of the game. See figure 2.7. This is essentially the same as the clearfield example, but with two buttons instead of one.
In figure 2.8, we have two sources of canned messages, and we use the new merge primitive to merge them together. We’ll draw merge as a trapezoid. (It was our child- hood wish to one day write a book with the word trapezoid in it.)
Figure 2.8 Merging two text streams into one
Listing 2.4 gives the code. STextField takes only one stream as input, so you need to merge the two into one. This example uses a Sodium variant of merge called .orElse(). This naming has to do with how it handles simultaneous events, which we’ll explain next.
import javax.swing.*;
import java.awt.FlowLayout;
import swidgets.*;
import nz.sodium.*;
Listing 2.4 Merging two canned message sources into one
Figure 2.7 Buttons for poking canned messages into the text field
sClicked sClicked SButton
STextField sCanned
SButton
39 The merge primitive: merging streams
public class gamechat {
public static void main(String[] args) { JFrame frame = new JFrame("gamechat");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setLayout(new FlowLayout());
SButton onegai = new SButton("Onegai shimasu");
SButton thanks = new SButton("Thank you");
Stream<String> sOnegai = onegai.sClicked.map(u ->
"Onegai shimasu");
Stream<String> sThanks = thanks.sClicked.map(u -> "Thank you");
Stream<String> sCanned = sOnegai.orElse(sThanks);
STextField text = new STextField(sCanned, "");
frame.add(text);
frame.add(onegai);
frame.add(thanks);
frame.setSize(400, 160);
frame.setVisible(true);
} }
Check it out with git, if you haven’t done so already, and run it like this:
git clone https://github.com/SodiumFRP/sodium cd sodium/book/swidgets/java
mvn test -Pgamechat or ant gamechat
DEFINITION The merge primitive puts the events from two streams together into a single stream. The name merge for this primitive is universal, but you may encounter the more mathematical terms union and append.
The two input streams and one output stream of the merge operation must all have the same type. In the example, Stream<String> is the type. merge gives you a stream such that if either of the input streams fires, an event will appear on the output stream at the same time.
2.6.1 Simultaneous events
We alluded earlier to the fact that FRP processing takes place in a transactional con- text. This is basically the same idea as the transactions used in databases.
How are transactions started in Sodium?
Sodium automatically starts a transaction whenever an input value is pushed into a stream or cell. Any state changes that occur as a result of that input are performed within the same transaction. Mostly you don’t need to do anything, but it’s possible to start a transaction explicitly.
For the reasons we’ve explained, we want to leave the explanation of how to push input values until a later chapter.
sOnegai and sThanks both come from the SButton class. We wrote SButton in such a way that it starts a new transaction for each button click event, so we happen to know that two different button events can’t occur in the same transaction. Thus sCanned will never encounter the situation where two events can occur in the same transaction.
DEFINITION Simultaneous events—Two or more stream events that occur in the same transaction. In Sodium, they’re truly simultaneous, because their order relative to each other can’t be detected.
It’s normal practice in FRP for each external event to run in a new transaction, so it’s normally OK to assume that no two events from external streams will be simultaneous.
This is what we did in SButton. But we can’t always assume events aren’t simultaneous.
Because external streams aren’t usually a source of simultaneity, simultaneous events are almost always caused by two streams that are modifications of a single input stream.
EXAMPLE: SIMULTANEOUS EVENTS IN A DRAWING PROGRAM
Let’s say you’re developing a program for drawing diagrams, in which graphical ele- ments can be selected or deselected. The rules are these:
■ If you click an item, it’s selected.
■ If an item is selected, and you click elsewhere, the item gets deselected.
Figure 2.9 illustrates us performing three steps with the diagram program:
1 At first nothing is selected, and we’re ready to click the triangle.
2 When we’ve clicked the triangle, it’s high- lighted.
3 We get ready to click the octagon.
At this point, a single mouse click will cause two simultaneous events to be generated:
■ Deselecting the triangle
■ Selecting the octagon
You’ll almost certainly want to merge these streams at some point in the program. Because these two events originate in the same mouse click event, they’re simultaneous. All three events—the mouse click, the deselect, and the select—are conceptually truly simultaneous in FRP, meaning it’s impossible to detect any ordering in their occurrence.
In appendix B, we use this example to illustrate the first plague of listeners: unpredictable order. You’ll find the code there.
Figure 2.9 Three steps in using the diagram program
41 The merge primitive: merging streams
DEALING WITH SIMULTANEOUS EVENTS
Each FRP system has its own policy for merging simultaneous events. Sodium’s policy is as follows:
■ If the input events on the two input streams are simultaneous, merge combines them into one. merge takes a combining function as a second argument for this purpose. The signature of the combining function is A combine(A left, A right).
■ The combining function is not used in the (usually more common) case where the input events are not simultaneous.
■ You invoke merge like this: s1.merge(s2, f). If merge needs to combine simul- taneous events, the event from s1 is passed as the left argument of the combin- ing function f, and the event from s2 is passed on the right.
■ The s1.orElse(s2) variant of merge doesn’t take a combining function. In the simultaneous case, the left s1 event takes precedence and the right s2 event is dropped. This is equivalent to s1.merge(s2, (l, r) -> l). The name orElse() was chosen to remind you to be careful, because events can be dropped.
This policy has some nice results:
■ There can only ever be one event per transaction in a given stream.
■ There’s no such thing as event-processing order within a transaction. All events that occur in different streams within the same transaction are truly simultane- ous in that there’s no detectable order between them.
Some formulations of FRP don’t force you to combine simultaneous events, so they allow more than one event per stream. But we think Sodium’s policy—forcing the decision for every merge—is the right thing to do because we get true event simulta- neity. This helps simplify the job of reasoning about logic.
NOTE Heinrich Apfelmus, author of the Reactive Banana FRP system, is also a proponent of forcing the programmer to combine simultaneous events for every merge.
How simultaneous events are handled depends only on things specified locally to merge, not on things in distant parts of the program. This guarantees compositional semantics, and compositionality is vital for reducing bugs. We’ll go into the reasons for this in chapter 5.
NOTE In some FRP-like systems, there is no concept of simultaneous events, so merge can’t be guaranteed to act consistently. This breaks compositional- ity, so these systems technically aren’t true FRP systems. At the time of writing, the system known as Reactive Extensions (Rx) and the many systems inspired by it don’t meet this requirement. We hope this will change because the prob- lems that result from it aren’t just theoretical.
2.6.2 Collection variants of merge
Sodium’s Stream class also provides the following variants of merge that work on col- lections of streams. Every FRP system has an equivalent:
static <A> Stream<A> orElse(java.lang.Iterable<Stream<A>> ss)
static <A> Stream<A> merge(java.lang.Iterable<Stream<A>> ss, Lambda2<A,A,A> f)
2.6.3 How does merge do its job?
How does merge do the job of combining simultaneous events? To answer this ques- tion, we’re going to descend once again into the fetid world of the operational.
The bottom of figure 2.10 shows the execution of the merge example from the pre- vious section operationally in sequence. You see a transaction executing in time.
Conceptually, the order of the events on sDeselect and sSelect isn’t detectable;
but here we show them occurring operationally in the opposite of the desired order.
The merge implementation has to store the events in temporary storage until a time when it knows it won’t receive any more input. Then it outputs an event: if it received more than one, it uses the supplied function to combine them; otherwise, it outputs the one event it received.
Figure 2.10 The mechanics of how merge deals with simultaneous events
Conceptual view
Execution in time sSelect
sSelect
sChange
sChange
sDeselect
sDeselect
left
left right
right
43 The hold primitive: keeping state in a cell