There are two views of time in FRP systems:
■ Discrete time
■ Continuous time
This difference pertains to cells only. Streams are the same in both models. Sodium is mostly focused on discrete time, but you can express continuous time in it, and we’ll cover this in detail in chapter 9.
In a discrete time system, state (cell) changes or steps happen in response to events, so the steps are discrete. A continuous time system allows this too, but it also has the ability for a cell’s value to vary continuously over time. An obvious case where this would be useful would be in simulating physics, as you might do in a video game.
To protect the idea of a continuously varying cell, a true FRP system must ensure that changes in a cell’s value aren’t observable. The 10 core primitives we’ve listed are con- sistent with this because they give you no way to convert a cell into a stream. Note that listen() is an operational external interface thing and is not considered part of FRP. We’re about to introduce two primitives that break that nice property. They’re sometimes needed for operational situations.
NOTE In Rx, we recommend BehaviorSubject to do the job of an FRP cell, but BehaviorSubject is not a distinct data type in Rx. It can be treated directly as an Observable (the same as Stream in Sodium). Unfortunately this means there’s no direct way to achieve the desirable property of concealing cell steps.
8.4.1 Introducing updates and value
The Sodium-specific primitives updates and value both take a cell and give you a stream. Here are two use cases for this. There aren’t many (some others will come up in later chapters):
■ You might want to send a cell over a network link. This would require decon- structing it into its current value and a stream of its updates, and holding the result on the other end of the wire.
■ In chapter 12, we’ll discuss a function called calm() that for performance rea- sons removes unnecessary steps from cells, where the value is a repeat of the previous firing. You need Operational.updates() to implement it, but it fol- lows our rule and doesn’t expose the steps to the caller.
We’ve given them the following deliberately cumbersome names because we want you always to be aware that by using them, you’re breaking the non-detectability of cell steps:
■ Operational.updates()
■ Operational.value()
NOTE In Rx, as we mentioned, BehaviorSubject (cell) and Observable (stream) aren’t distinct types. This means no equivalent of updates and value is needed to convert one to the other.
updates gives you the discrete updates to a cell. value differs from updates by firing once with the cell’s current value in the transaction where it was invoked. updates is effectively the inverse of hold. To preserve the non-observability of cell steps, the gen- eral rule is this:
Functions that use Operational.updates() shouldn’t expose cell steps to the caller.
NOTE Some systems use the term changes for updates, but it’s important to note that they’re not changes in the sense of being values that aren’t equal.
That is, Sodium doesn’t filter out values that are equal to the previous value.
If you want this functionality, you can write it yourself with FRP primitives.
EXAMPLE OF UPDATES
Listing 8.4 listens to the updates of a cell. The key thing to note is that this doesn’t give you the current value in the first callback like when you called a cell’s listen() method in an earlier example. In this example, the current value of x is 1 when you listen, but you don’t see 1 in the output because updates only captures the updates that occur in the same transaction as or after you start listening.
import nz.sodium.*;
public class updates {
public static void main(String[] args) { CellSink<Integer> x = new CellSink<>(0);
Listing 8.4 Listening to the updates of a cell
179 Getting a stream from a cell with updates and value
x.send(1);
Listener l = Operational.updates(x).listen(x_ -> { System.out.println(x_);
});
x.send(2);
x.send(3);
l.unlisten();
} }
ant updates updates:
[java] 2 [java] 3
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 -Pupdates or ant updates EXAMPLE OF VALUE
Listing 8.5 doesn’t give the effect you may expect. The reason is that it doesn’t specify an explicit transaction. If a transaction doesn’t already exist, Sodium primitives create a short-lived one automatically, so value() and listen() run in two separate transac- tions. By the time you listen, you’ve missed the current value that was output by value().
import nz.sodium.*;
public class value1 {
public static void main(String[] args) { CellSink<Integer> x = new CellSink<>(0);
x.send(1);
Listener l = Operational.value(x).listen(x_ -> { System.out.println(x_);
});
x.send(2);
x.send(3);
l.unlisten();
} }
ant value1
value1:
[java] 2 [java] 3
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 -Pvalue1 or ant value1
Listing 8.5 value not working as expected
What? Where’s the current value?
To solve this, you wrap this line in a transaction, which you’ll find in the file value2.java:
x.send(1);
Listener l = Transaction.run(() -> {
return Operational.value(x).listen(x_ -> { System.out.println(x_);
});
});
x.send(2);
x.send(3);
Now you get the output you expect:
ant value2 value2:
[java] 1 [java] 2 [java] 3
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 -Pvalue2 or ant value2
Note that Cell’s listen() is equivalent to value().listen() in a transaction. It’s a shorter way to do what you’ve done here. You can rewrite the earlier line as
Listener l = x.listen(x_ -> { System.out.println(x_); });