But what if you throw actor a curve ball? You’re going to give it a second state variable that doesn’t fit neatly with dragging:
■ If you hold down the Shift key, the element’s displacement is locked to the X or Y axis.
You should be familiar with this because most diagramming software has this feature.
Listing 10.7 gives the actor version with changes in bold. There are two problems:
■ The code to change the axisLock state is duplicated. Of course, this can be put into a method, but it still has to be handled separately in both loops.
■ In actor, it’s common to use dynamic typing on input messages, so we’ve done that here. The disadvantage is that it’s possible to accidentally send an object of a type that an actor doesn’t know about, or forget to handle a type somewhere in your actor. For example, you could accidentally forget about Type (Shift key) messages in one of the two places.
Actor can flatten a state variable into a flow of control, and you’ve seen that this is a powerful technique. But it can only do this successfully with one state variable. We flat- tened dragging into the flow of control, and document worked nicely along with it. But axisLock doesn’t fit this flow at all. Hence, the code is duplicated.
class Actor implements Paradigm {
public Actor(Document initDoc, DocumentListener dl) {
ArrayBlockingQueue<Document> out = new ArrayBlockingQueue<>(1);
t1 = new Thread(() -> { try {
Document doc = initDoc;
boolean axisLock = false;
while (true) {
MouseEvt me1 = null;
Entry ent = null;
while (true) {
Object o = in.take();
if (o instanceof MouseEvt) { MouseEvt me = (MouseEvt) o;
Listing 10.7 Drag-and-drop with axis lock: actor model version
The input value is now dynamically typed.
209 Let’s add a feature: Shift key gives axis lock
if (me.type == Type.DOWN) {
Optional<Entry> oe = doc.getByPoint(me.pt);
if (oe.isPresent()) { me1 = me;
ent = oe.get();
break;
} } }
if (o instanceof Type) { Type t = (Type) o;
axisLock = t == Type.DOWN;
} }
System.out.println("actor dragging " + ent.id);
while (true) {
Object o = in.take();
if (o instanceof MouseEvt) { MouseEvt me = (MouseEvt) o;
if (me.type == Type.MOVE) { doc = doc.insert(ent.id,
ent.element.translate(me1.pt, me.pt, axisLock));
out.put(doc);
} else
if (me.type == Type.UP) break;
}
if (o instanceof Type) { Type t = (Type) o;
axisLock = t == Type.DOWN;
} } }
} catch (InterruptedException e) {}
});
...
}
private final Thread t1, t2;
private final ArrayBlockingQueue<Object> in =
new ArrayBlockingQueue<>(1);
public void mouseEvent(MouseEvt me) { try {
in.put(me);
} catch (InterruptedException e) {}
}
public void shiftEvent(Type t) { try {
in.put(t);
} catch (InterruptedException e) {}
}
public void dispose() { t1.interrupt(); t2.interrupt(); } }
Shift key handling is duplicated.
Irrelevant code omitted
The next listing shows the same logic change in the FRP version. You can see that the change is fairly trivial.
class FRP implements Paradigm {
public FRP(Document initDoc, DocumentListener dl) { l = Transaction.run(() -> {
CellLoop<Document> doc = new CellLoop<>();
Cell<Boolean> axisLock = sShift.map(t -> t == Type.DOWN) .hold(false);
Stream<Stream<Document>> sStartDrag = Stream.filterOptional(
sMouse.snapshot(doc, (me1, doc1) -> { if (me1.type == Type.DOWN) {
Optional<Entry> oe = doc1.getByPoint(me1.pt);
if (oe.isPresent()) { String id = oe.get().id;
Element elt = oe.get().element;
System.out.println("FRP dragging " + id);
Stream<Document> sMoves = sMouse .filter(me -> me.type == Type.MOVE) .snapshot(doc, (me2, doc2) ->
doc2.insert(id,
elt.translate(me1.pt, me2.pt, axisLock.sample())));
return Optional.of(sMoves);
} }
return Optional.empty();
}));
Stream<Document> sIdle = new Stream<>();
Stream<Stream<Document>> sEndDrag =
sMouse.filter(me -> me.type == Type.UP) .map(me -> sIdle);
Stream<Document> sDocUpdate = Cell.switchS(
sStartDrag.orElse(sEndDrag).hold(sIdle) );
doc.loop(sDocUpdate.hold(initDoc));
return sDocUpdate.listen(doc_ -> dl.documentUpdated(doc_));
});
}
private final Listener l;
private final StreamSink<MouseEvt> sMouse = new StreamSink<>();
public void mouseEvent(MouseEvt me) { sMouse.send(me); } private final StreamSink<Type> sShift = new StreamSink<>();
public void shiftEvent(Type t) { sShift.send(t); } public void dispose() { l.unlisten(); }
}
To run this, 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 -Pshift or ant shift
Listing 10.8 Drag-and-drop with axis lock: FRP version
211 Improvement: Shift key updates the document
The classic implementation, which we don’t list here, is also a trivial change. We think FRP still beats classic because of the elimination of the invalid state. You’ll find this code under sodium/book/battle/java/shift/.