Looping hold and snapshot to

Một phần của tài liệu Manning functional reactive programming (Trang 70 - 75)

To illustrate how you can accumulate state changes, let’s implement a simple spinner—see figure 2.16. The accumu- lated value is kept in a cell, and you use an SWidgetsSLabel to display it.

Figure 2.17 shows the conceptual view.

The accumulator is circled.

Figure 2.17 Conceptual view of accumulating + and - button clicks, with the accumulator circled

2.9.1 Forward references

Before we give the complete listing, we need to have a little chat about forward refer- ences. Are you sitting comfortably? Then we’ll begin.

You start by making a stream of deltas of 1 when + is clicked and -1 when - is clicked, like this:

Stream<Integer> sPlusDelta = plus.sClicked.map(u -> 1);

Stream<Integer> sMinusDelta = minus.sClicked.map(u -> -1);

Stream<Integer> sDelta = sPlusDelta.orElse(sMinusDelta);

All you have to do is accumulate the sDeltas:

Stream<Integer> sUpdate = sDelta.snapshot(value, (delta, value_) -> delta + value_

);

Cell<Integer> value = sUpdate.hold(0);

Figure 2.16 A spinner example: + increments the value, and - decrements it.

Accumulator SButton

SButton

SLabel sPlusDelta

sMinusDelta

IblValue

toString sClicked

value sClicked

Unfortunately, Java won’t let you write this, because you’re defining value in terms of itself. value depends on sUpdate, and sUpdate depends on value.

DEFINITION Value loop—In functional programming, a value (in this case, a stream or cell) defined directly or through other variables in terms of itself.

In our youth, we considered it important to make our breakfast before eating it. We recognized long ago that there was a dependency relationship between the two. But only in a Euclidean universe must one be temporally before the other, which is to say we prefer to think in terms of dependency rather than sequence.

Java isn’t entirely on board with this way of thinking, so in Sodium we use a trick called CellLoop to get around it. Here’s the Sodium code:

CellLoop<Integer> value = new CellLoop<>();

Stream<Integer> sUpdate = sDelta.snapshot(value, (delta, value_) -> delta + value_

);

value.loop(sUpdate.hold(0));

CellLoop is like an immutable variable that you assign once through its loop() method. But unlike a normal variable, you can reference it before it’s assigned.

NOTE For streams, there is a corresponding StreamLoop.

CellLoop and StreamLoop are subclasses of Cell and Stream. They’re equivalent in every way to the Cell or Stream assigned to them using loop()—and the key idea is that you can use them freely before loop() is called. The only purpose they serve is to make forward references possible. But we take it one step further and allow cycles in variable references. This may look like black magic, and it is, but we assure you it isn’t complex. It only looks that way because your thoughts are cluttered with complexities relating to execution sequence. Banish them from your mind.

NOTE FRP code exists outside of time. Less poetically, there is no concept of sequence in FRP statements. You could take a blob of FRP code, arrange the lines in a random order, and insert CellLoops and StreamLoops as needed, and the code would work exactly the same. We call this the “sea sponge in a blender” principle, because it’s said that if you put a sea sponge through a blender, it can reassemble itself. We don’t really believe this.

2.9.2 Constructing FRP in an explicit transaction

The recommended practice with Sodium is to construct the FRP logic for a program under a single big, explicit transaction. In most cases, Sodium will start transactions automatically for you as needed. But CellLoop and StreamLoop are sensitive petals and require the declaration and the .loop() call to be in the same transaction. They will throw an exception if this isn’t done.

49 Looping hold and snapshot to create an accumulator

To wrap some code in an explicit transaction, you can write this (we’re using Java 8 lambdas again):

Transaction.runVoid(() -> { ... your code ...

}

If you want to return a value from the transactional block, do this:

A a = Transaction.run(() -> { ... your code ...

A a = ...;

return a;

}

DEFINITION Loan pattern—Method where you pass a lambda to some function that opens and closes a resource for you. It’s often used with files. This is a good idea because it’s impossible to accidentally forget to close the resource.

If this isn’t making sense, please skip ahead to the more detailed explanation of the loan pattern in section 8.3; that section also covers transactions in more detail.

2.9.3 Accumulator code

The following listing puts together all the elements of the accumulator.

import javax.swing.*;

import java.awt.FlowLayout;

import swidgets.*;

import nz.sodium.*;

public class spinner {

public static void main(String[] args) { JFrame view = new JFrame("spinner");

view.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

view.setLayout(new FlowLayout());

Transaction.runVoid(() -> { CellLoop<Integer> value = new CellLoop<>();

SLabel lblValue = new SLabel(

value.map(i -> Integer.toString(i)));

SButton plus = new SButton("+");

SButton minus = new SButton("-");

view.add(lblValue);

view.add(plus);

view.add(minus);

Stream<Integer> sPlusDelta = plus.sClicked.map(u -> 1);

Stream<Integer> sMinusDelta = minus.sClicked.map(u -> -1);

Stream<Integer> sDelta = sPlusDelta.orElse(sMinusDelta);

Stream<Integer> sUpdate = sDelta.snapshot(value, (delta, value_) -> delta + value_

);

Listing 2.7 Accumulating + and - button clicks

Wraps code in an explicit transaction

Forward reference

value.loop(sUpdate.hold(0));

});

view.setSize(400, 160);

view.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 -Pspinner or ant spinner

DEFINITION Accumulator—A piece of state that’s updated by combining new information with the existing state.

We’ve shown you how to build an accumulator from three basic elements: hold, snap- shot, and loops or forward references. Sodium and other FRP systems give you a more convenient way to write accumulators through methods with names like accum() and collect(), but they are just helpers based on these three elements.

2.9.4 Does snapshot see the new value or the old value?

When an event enters your FRP logic through a stream, it causes a cascade of state changes. As we explained, this happens under a transactional context, so all the state changes that result from a given event are made atomically.

DEFINITION If a set of state changes is made atomically, it means to all appear- ances they’re made at the same time. That is, it’s impossible to observe a situ- ation where some have been made but not others. The property of atomicity can save you from a cosmos of lamentation.

In the last example, we looked at an accumulator, where you read from a cell using snapshot and wrote to it using hold in the same transaction. Does snapshot see the new value or the old value of value? In this case, it would be impossible to update value before reading it because the new value to be written depends on what was read. It would eat its own tail and implode to a singularity.

But what about the more general case, where the same event—implying the same transaction—causes cell A both to be updated through a hold and also to be snap- shotted? Does snapshot see cell A before or after the update?

Each FRP system takes a different approach to this problem. Sodium works the same as Conal Elliott’s formulation of FRP, saying that snapshot always sees the old value. You can view this in two equivalent ways. Either

■ snapshot sees the value as it was at the beginning of the transaction.

or

■ hold updates are performed atomically at the end of the transaction. This is how we’ve shown it in figure 2.18.

51 Looping hold and snapshot to create an accumulator

Figure 2.18 shows the execution sequence of hold committing its updates at the end of the transaction. Here value is 5 and the user clicks +.

Here are some approaches you might see in other FRP systems:

■ Some say their snapshot equivalent sees the new value and will behave unpre- dictably if there’s a loop.

■ Some normally give the new value but have an explicit delay primitive so you can get the old value. An accumulator is expressed as a loop of hold-delay- snapshot.

■ Some don’t allow value loops and provide only higher-level accum-style primitives.

So delay can be seen as a separate primitive, but in Sodium it’s implicit.

The need for a non-delaying hold

Sodium’s hold has an implicit delay. Some people argue that a non-delaying hold is also needed in some circumstances. We’ve done quite well without it. We aren’t convinced that a non-delaying hold is necessary, but we’ll leave this for you to con- template.

Stream

Cells are updated atomically at

end of transaction Cell always

has a value

Conceptual view

Execution in time

Figure 2.18 The execution sequence of updating an accumulator: value is 5, and the user clicks +.

Một phần của tài liệu Manning functional reactive programming (Trang 70 - 75)

Tải bản đầy đủ (PDF)

(362 trang)