Improvement: Shift key updates the document

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

In the previous Shift key/axis lock example, the change in the Shift key state doesn’t cause the dragged element to be redrawn. You have to move the mouse infinitesimally to do it, and this feels wrong to the user. It’d be nice to fix each paradigm so shift- Event() also causes the element position to be updated.

We’ve implemented a third version of the code that does this. We’ll give code snip- pets to illustrate the changes you need to make. You can read the full code at sodium/

book/battle/java/shift2/.

To run this version, 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/battle/java

mvn test -Pshift2 or ant shift2

10.3.1 Changing this in the classic paradigm

You need to add a new move field to remember the most recent move position, and you need to set it to an initial value in UP and then update it in MOVE. The output of the updated document is moved to a new updateMove() method:

private Point move;

...

case DOWN:

...

if (...) {

move = me.pt;

} break;

case MOVE:

move = me.pt;

updateMove();

break;

shiftEvent() is changed to call updateMove():

public void shiftEvent(Type t) { axisLock = t == Type.DOWN;

updateMove();

}

The extra state variable move makes us a little uneasy. It belongs in the Dragging data structure. This would be cleaner, except that Optional isn’t designed to be modified in place. We’ll leave you to think about this.

10.3.2 Changing this in FRP

In FRP, you also need some new state to keep the most recent move position. The best way to do this is to model move and axisLock as cells instead of streams, because they’re both stateful. You define axisLock like this at the top level:

Cell<Boolean> axisLock = sShift.map(t -> t == Type.DOWN) .hold(false);

On the start of the drag state, you define move—the most recent mouse move posi- tion—like this:

Cell<Point> move =

sMouse.filter(me -> me.type == Type.MOVE) .map(me -> me.pt)

.hold(me1.pt);

NOTE We’ve talked about this before: the fact that we’re using switch means the transactional context in which the holds are executed is important. axis- Lock is in the transactional context of all the logic because it should be inde- pendent of when the drag starts and stops. But move must be defined in the transactional context of handling the mouse DOWN because it should exist dur- ing the dragging state only. Its initial value comes from the position of the DOWN event, which is only in scope within this transactional context.

Now it’s simple to use lift to calculate where the dragged element should be:

Stream<Document> sMoves = Operational.updates(

move.lift(axisLock, (mv, lck) ->

elt.translate(me1.pt, mv, lck)) )

.snapshot(doc, (newElt, doc2) ->

doc2.insert(id, newElt));

Finally, you break the rule of non-observability of cell steps (see section 8.4) and use Operational.updates() to turn that cell into a stream of the new element to be replaced in the document. This is a neat way to solve the problem, so we’ll leave you to contemplate the philosophical issues yourself. Bear in mind that some FRP systems may not allow you to do this.

If you want to avoid this, it can be done in an equivalent but less neat way with two snapshots. For move and axisLock, you need both the stream and cell variants, so you need move, sMove, axisLock, and sAxisLock. When sMove fires, you want to capture the most recent axisLock, and vice versa with sAxisLock and move. So you merge an sMove.snapshot(axisLock) with an sAxisLock.snapshot(move). Here it is, imple- mented in a second Paradigm implementation called FRP2:

class Pair {

Pair(Point move, boolean lock) { this.move = move;

this.lock = lock;

}

213 Improvement: Shift key updates the document

Point move;

boolean lock;

}

Stream<Point> sMove =

sMouse.filter(me -> me.type == Type.MOVE) .map(me -> me.pt);

Cell<Point> move = sMove.hold(me1.pt);

Stream<Pair> sPair = sMove.snapshot(axisLock, (m, l) -> new Pair(m, l))

.orElse(sAxisLock.snapshot(move, (l, m) -> new Pair(m, l)));

Stream<Document> sMoves = sPair.snapshot(doc, (p, doc2) -> doc2.insert(id,

elt.translate(me1.pt, p.move, p.lock)));

10.3.3 Changing this in the actor model

The change to the actor code is fairly straightforward. A document update can be trig- gered by two different events, so you use a temporary variable, toUpdate, for that:

Point move = me1.pt;

while (true) {

Object o = in.take();

boolean toUpdate = false;

if (o instanceof MouseEvt) { MouseEvt me = (MouseEvt) o;

if (me.type == Type.MOVE) { move = me.pt;

toUpdate = true;

} else

if (me.type == Type.UP) break;

}

if (o instanceof Type) { Type t = (Type) o;

axisLock = t == Type.DOWN;

toUpdate = true;

}

if (toUpdate) {

doc = doc.insert(ent.id,

ent.element.translate(me1.pt, move, axisLock));

out.put(doc);

} }

10.3.4 How are the different paradigms doing?

Let’s reflect on how each paradigm is coping with what we’ve thrown at it:

Classic—You’ve got to make sure you check whether you’re in the dragging state, and it’s also possible to inadvertently forget to call updateMove(). The classic paradigm often leads you to have one or more update() methods that

bring things up to date and generate outputs. Forgetting to call update() is a common bug that can’t occur in FRP.

FRP—It coped well. It got verbose in one case when we tried to avoid the short- cut of using Operational.updates(), but it remained semantically tidy.

Actor—The biggest problem was that we had to handle the Type (Shift key up/

down) message twice and duplicate some logic, and there doesn’t seem to be an easy way to solve this. Actor can also lead you into long methods with lots of local variables, and the example is getting there. Perhaps this can be split into two actors, but it isn’t clear how to do this. The actor code also suffers a little from the same “update-itis” that classic has: it would be easy to inadvertently forget to set toUpdate. In FRP, the cell abstraction is effective at eliminating this problem.

10.3.5 State machines with long sequences

We’ll reiterate something we’ve said before. FRP isn’t ideal for things that naturally fall into a long linear sequence of state changes—that is, when a single state variable goes through a long, complex, branching and/or looping sequence. An example might be communicating with an SMTP server to send a list of emails and handling errors at each step. Actor/threads would be a more suitable abstraction for this kind of logic.

FRP is better for situations with multidimensional state transitions: a large number of state variables interacting in complex ways. We’ve given many examples where FRP is sensible.

So, even though FRP does well in the current example, there do exist nontrivial problems where actor would be more suitable than FRP. We like the idea of a combina- tion of FRP and actors for some applications.

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

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

(362 trang)