Now you now know about the map, merge, hold, snapshot, filter, lift, never, and constant primitives. That’s all you need to build the petrol pump. There are only two core primitives to go: sample and switch. You don’t need them for this example, so we’ll get to them in chapter 7.
The first thing you’ll do is translate the nozzle input events into the life cycle of a pump fill, as shown in figure 4.6. The outputs are as follows:
■ sStart—A stream indicating the start of the fill
■ fillActive—A cell telling you whether a fill is currently active and, if so, what fuel was selected
■ sEnd—A stream indicating the end of the fill Here are the requirements:
■ When a nozzle is lifted, you select a fuel depending on the nozzle that was cho- sen.
■ When that nozzle (and only that noz- zle) is hung up, the fill ends. The other two nozzles are ignored while the fill is in progress.
As we mentioned, fillActive is a cell rep- resenting whether you’re filling or not. Its type is Cell<Optional<Fuel>>, using Java
8’s Optional type (see the following sidebar), and an enum called Fuel, identifying which fuel was selected. The possible values of fillActive are these:
■ Optional.empty()—You aren’t filling.
■ Optional.of(fuel)—You’re filling, and the selected fuel is fuel.
Java 8’s Optional type
Java 8 defines a new class called Optional under java.util. It takes a type param- eter. Optional<A> represents a nullable value of type A; that is, either it contains a value of type A or it has a “nothing” value.
It’s a replacement for the traditional approach of using a null reference. If you con- sistently use Optional instead of null, then whether a value is nullable is captured in the type; thus it’s much harder to accidentally forget to deal with the null case.
Forgetting to check for null is a common source of bugs. Tony Hoare, who invented the null reference in 1965, calls it his “billion dollar mistake.”
We won’t go into any more detail here. If this doesn’t explain things well enough, online resources will probably be all you need. We’re not trying to be Java 8 power users, but a couple of Java 8 features turn out to be compelling for our purposes. For a more complete treatment of Java 8, see Java 8 in Action by Raoul-Gabriel Urma, Mario Fusco, and Alan Mycroft (Manning, 2014, www.manning.com/books/java-8-in-action).
sNozzle1
LifeCycle
sStart
sEnd fillActive sNozzle2
sNozzle3
Figure 4.6 Inputs and outputs of a LifeCycle class
Figure 4.7 zooms in on the implementation of LifeCycle in the style of one of the familiar conceptual diagrams from chapter 2. You’ve seen each of the primitives used already, but now more of them are put together.
We’ve left some of the logic out of this diagram and drawn it as clouds or black boxes. We haven’t diagrammed these two, so look at how they’re implemented as methods in the code:
■ whenLifted—Takes a constant value of a fuel number (ONE, TWO, or THREE), and, when that nozzle is lifted, outputs that constant.
■ whenSetDown—Also takes a constant fuel number. In addition, looks at fill- Active, the currently selected fuel, and only outputs an event if a fill is active and the nozzle that was set down matches the fuel of the current fill.
Here’s the flow of events when nozzle 1 is lifted:
1 The stream returned by whenLifted(Fuel.ONE) outputs an event containing the value Fuel.ONE.
2 This event causes a snapshot of the current value of fillActive and is filtered so that it only outputs the fuel number if there isn’t already a fill active. This is exported as the stream sStart.
3 sStart feeds through a map that wraps the event value inside an Optional type so the value becomes Optional.of(Fuel.ONE).
4 This is held by fillActive so the fill state becomes active with a selected fuel of Fuel.ONE.
sNozzle1
sStart
sEnd fillActive fillActive
Optional.of Opt.empty
Only triggers when the nozzle matches fillActive
fill not active sNozzle2
sNozzle3
whenLifted Fuel.ONE
whenLifted Fuel.TWO
whenLifted Fuel.THREE
whenSetDown Fuel.THREE whenSetDown
Fuel.TWO whenSetDown
Fuel.ONE sNozzle1
sNozzle2
sNozzle3
Figure 4.7 Conceptual overview of the logic for LifeCycle
75 The life cycle of a petrol pump fill
When nozzle 1 is set back down, this happens:
■ whenSetDown(Fuel.ONE) checks that a fill is active on Fuel.ONE, and, if so, it outputs an event. This is exported as the stream sEnd.
■ sEnd then feeds through a map that turns it into an Optional value of Optional.empty().
■ This is held by fillActive so the fill state becomes inactive.
4.4.1 Code for LifeCycle
This section presents two classes. The logic is given in the LifeCycle class in listing 4.4. A bit later, listing 4.5 will give a second class, LifeCyclePump, that connects the logic to the pump inputs and outputs so you can run it.
Whenever you want to get an initial understanding of an FRP-based class or method, we suggest you start by looking at what the inputs and outputs are, with an understanding of the stated intent. Then, slowly examine how the inputs are trans- formed into outputs.
Fuel is an enum defined like this, used to identify which of the three fuel nozzles was selected:
public enum Fuel { ONE, TWO, THREE }
If you look at the code, you can see that sStart and sEnd depend on fillActive, which depends back on sStart and sEnd. As we discussed in chapter 2, CellLoop is the magic trick to deal with these cyclic dependencies.
Recall that StreamLoop and CellLoop must be loop()ed in the same transaction as they’re constructed. In this example, we’ve made this issue go away: the petrol pump simulator main program—which we haven’t listed in the book—wraps the invocation of create() in a transaction. This is how we always recommend constructing FRP logic, and you should normally assume this for any FRP construction code.
NOTE This code uses a variant of filter that you haven’t seen much of yet:
filterOptional. It takes a Stream<Optional<A>> and gives a Stream<A>. Java 8’s Optional is useful again. Where the value is present, filterOptional unwraps the value and propagates it to the new stream as a value of type A.
Events where the value is empty are filtered out (they don’t propagate).
Because the input stream has an Optional type, Java won’t let you declare it as a method of Stream; it’s a static method, instead, and hence Stream .filterOptional(..).
package chapter4.section4;
import pump.*;
import nz.sodium.*;
import java.util.Optional;
Listing 4.4 Life cycle of a petrol pump fill
public class LifeCycle {
public final Stream<Fuel> sStart;
public final Cell<Optional<Fuel>> fillActive;
public final Stream<End> sEnd;
public enum End { END }
private static Stream<Fuel> whenLifted(Stream<UpDown> sNozzle, Fuel nozzleFuel) {
return sNozzle.filter(u -> u == UpDown.UP) .map(u -> nozzleFuel);
}
private static Stream<End> whenSetDown(Stream<UpDown> sNozzle, Fuel nozzleFuel,
Cell<Optional<Fuel>> fillActive) { return Stream.<End>filterOptional(
sNozzle.snapshot(fillActive,
(u,f) -> u == UpDown.DOWN &&
f.equals(Optional.of(nozzleFuel)) ? Optional.of(End.END) : Optional.empty()));
}
public LifeCycle(Stream<UpDown> sNozzle1, Stream<UpDown> sNozzle2, Stream<UpDown> sNozzle3) { Stream<Fuel> sLiftNozzle = whenLifted(sNozzle1, Fuel.ONE).orElse(
whenLifted(sNozzle2, Fuel.TWO).orElse(
whenLifted(sNozzle3, Fuel.THREE)));
CellLoop<Optional<Fuel>> fillActive = new CellLoop<>();
this.fillActive = fillActive;
this.sStart = Stream.filterOptional(
sLiftNozzle.snapshot(fillActive, (newFuel, fillActive_) ->
fillActive_.isPresent() ? Optional.empty()
: Optional.of(newFuel)));
this.sEnd = whenSetDown(sNozzle1, Fuel.ONE, fillActive).orElse(
whenSetDown(sNozzle2, Fuel.TWO, fillActive).orElse(
whenSetDown(sNozzle3, Fuel.THREE, fillActive)));
fillActive.loop(
sEnd.map(e -> Optional.<Fuel>empty()) .orElse(sStart.map(f -> Optional.of(f))) .hold(Optional.empty())
);
} }
The next listing gives a petrol pump logic implementation that tests this out. Try it in the pump simulator, and you’ll see that the number 1, 2, or 3 appears on the Liters display to indicate which nozzle has been lifted.
Start of the fill stream If a fill is active,
identifies the selected fuel
End of the fill stream
Only allows “up” to pass through from up/down events
Outputs what fuel this nozzle corresponds to End instead
of Unit for type safety
Only the nozzle matching the current fill can end the fill.
This stream fires when a nozzle is lifted, along with an identifier of which nozzle it was.
Declares a fillActive forward reference Start of fill can
only happen when a fill isn’t already in progress.
Checks each nozzle to see if it’s ending the fill
fillActive implementation Cleared at the
end of the fill
fillActive is set to the selected fuel at the start of the fill.
77 Is this really better?
package chapter4.section4;
import pump.*;
import nz.sodium.*;
import java.util.Optional;
public class LifeCyclePump implements Pump { public Outputs create(Inputs inputs) {
LifeCycle lc = new LifeCycle(inputs.sNozzle1, inputs.sNozzle2, inputs.sNozzle3);
return new Outputs()
.setDelivery(lc.fillActive.map(
of ->
of.equals(Optional.of(Fuel.ONE)) ? Delivery.FAST1 : of.equals(Optional.of(Fuel.TWO)) ? Delivery.FAST2 : of.equals(Optional.of(Fuel.THREE)) ? Delivery.FAST3 : Delivery.OFF)) .setSaleQuantityLCD(lc.fillActive.map(
of ->
of.equals(Optional.of(Fuel.ONE)) ? "1" : of.equals(Optional.of(Fuel.TWO)) ? "2" : of.equals(Optional.of(Fuel.THREE)) ? "3" : ""));
} }
Take this code for a spin. You’ve probably already checked it out, so you won’t need the git line, but here’s the complete list of commands:
git clone https://github.com/SodiumFRP/sodium cd sodium/book/petrol-pump/java
mvn test or ant run