-
Notifications
You must be signed in to change notification settings - Fork 48
How to Compose an EventStream Well
It's easy to compose EventStream objects. That doesn't mean one will do it well. Here's the process one should follow to compose one well:
-
Define the final EventStream.
Before thinking about what one will need to do to compose an EventStream, what Event should the final one emit and under what circumstances?
-
Define its dependencies recursively.
Once the final EventStream is defined, what EventStreams would need to be combined/composed together to create the final one? Once these secondary EventStreams have been defined, what are their dependencies? Continue defining a dependency's dependencies recursively until getting to a "leaf".
-
Write the code
Until the other two steps are accomplished, there's no reason to write any code. If one doesn't follow this model, the developer will waste time trying out various compositions of EventStreams until one actually works.
-
Maximize Composition, Minimize Subscriptions:
Only subscribe to EventStreams as needed. Every time one subscribes to an EventStream, it returns an additional Subscription object that needs to be properly managed in order to prevent a memory leak. Create a final EventStream to which one will subscribe and use composition everywhere else.
-
Draw Marble Diagrams:
EventStream composition becomes confusing pretty quickly if only words are used, but they become very clear when pictures are used: draw them out on a paper or whiteboard.
-
Reuse EventStream Dependencies:
If one EventStream will be the dependency of two or more others, then assign it to a local variable and use that variable rather than creating two or more copies of the same stream.
-
Use
conditionOn()
upstream rather than downstream:Let's say the initial event that gets emitted is the highest part of the stream and the subscription is the lowest part of the stream. If the pathway that an Event will follow has a
conditionOn()
gate somewhere in it, that gate should be as high up the stream as possible. This will prevent unneeded code from running, which wastes time.
I want a rectangle to change its fill based on a few situations:
Scenario # | Fill Color | Mouse Hovering over Rectangle? | ControlDown? | ShiftDown? | Transfer Mode Enabled? |
---|---|---|---|---|---|
1 | Red | N | N | N | N |
2 | Yellow | Y | N | N | N |
3 | White | Y | Y | N | N |
4 | Gray | Y | N | Y | Y |
5 | Purple | Y | Y | Y | N |
6 | Maroon | Y | Y | Y | Y |
What would the final EventStream be?
An EventStream that feeds its Paint events into rectangle's fill property. (Shape's fill property takes a Paint argument).
Thus, the final stream should be
EventStream<Paint> fillChanger = // code...
Subscription filler = fillChanger.feedTo(rectangle.fillProperty());
What is the final stream's dependencies?
One way is to say it has 6, one for each scenario. In that case, the final stream would merge all 6 scenario streams, so that fillChanger
emits the same event as any of its the scenario EventStreams:
EventStream<Paint> fillChanger = EventStreams
.merge(scenario1, scenario2, scenario3, scenario4, scenario5, scenario6);
Subscription filler = fillChanger.feedTo(rectangle.fillProperty());
How would each of these need to be constructed then?
Looking at scenario1
...scenario6
, all of them share the same conditions: hovering? controlPressed? shiftPressed? transferEnabled? Thus, all of them have the same dependencies but emit different Colors when certain booleans are true and others are false. So, let's assume that such an EventStream with these dependencies called combo
exists:
// Tuple4< hovering?, controlPressed? shiftPressed? transferEnabled? >
EventStream<Tuple4<Boolean, Boolean, Boolean, Boolean>> combo = // code...
EventStream<Paint> scenario1 = combo
.filter(tuple4 -> {
boolean hovering = tuple4.get1();
boolean controlPressed = tuple4.get2();
boolean shiftPressed = tuple4.get3();
boolean transferEnabled = tuple4.get4();
return !hovering && !controlPressed && !shiftPressed && !transferEnabled;
})
.map(x -> Color.RED);
EventStream<Paint> scenario2 = combo
.filter(t -> t.get1() && !t.get2() && !t.get3() && !t.get4())
.map(x -> Color.YELLOW);
EventStream<Paint> scenario3 = combo
.filter(t -> t.get1() && t.get2() && !t.get3() && !t.get4())
.map(x -> Color.WHITE);
EventStream<Paint> scenario4 = combo
.filter(t -> t.get1() && !t.get2() && t.get3() && t.get4())
.map(x -> Color.GRAY);
EventStream<Paint> scenario5 = combo
.filter(t -> t.get1() && t.get2() && t.get3() && !t.get4())
.map(x -> Color.PURPLE);
EventStream<Paint> scenario6 = combo
.filter(t -> t.get1() && t.get2() && t.get3() && t.get4())
.map(x -> Color.MAROON);
Let's be honest, the above code is nothing more than taking combo
and running a few if-condition-then-Color statements.
Why not simplify it? Let's map combo
to the correct Paint:
EventStream<Paint> fillChanger = combo
.map(tuple4 -> {
boolean hovering = tuple4.get1();
boolean controlPressed = tuple4.get2();
boolean shiftPressed = tuple4.get3();
boolean transferEnabled = tuple4.get4();
if (!hovering && !controlPressed && !shiftPressed && !transferEnabled;) {
// scenario 1
return Color.RED;
} else if (hovering) {
if (!controlPressed && !shiftPressed && !transferEnabled;) {
// scenario 2
return Color.YELLOW;
} else if (controlPressed && !shiftPressed && !transferEnabled;) {
// scenario 3
return Color.WHITE;
} else if (!controlPressed && shiftPressed && transferEnabled;) {
// scenario 4
return Color.GRAY;
} else if (controlPressed && shiftPressedO {
if (transferEnabled) {
// scenario 5
return Color.PURPLE;
} else {
// scenario 6
return Color.MAROON;
}
}
}
});
Not only is that less code, it's also easier to read. However, this could be further simplified. Let's remove fillChanger
entirely:
Subscription filler = combo
.map(tuple4 -> { /* if-condition-then-Color code... */ })
.feedTo(rectangle.fillProperty());
What about the dependency combo
? How does it get constructed?
Since combo
is a Tuple4 of Booleans, it would need to combine 4 EventStreams:
// hovers is a "leaf!"
EventStream<Boolean> hovers = EventStreams.nonNullValuesOf(rectangle.hoverProperty());
// control/shiftPresses emits "true" when their key is pressed and "false" when it is released.
EventStream<Boolean> controlPresses = // code
EventStream<Boolean> shiftPresses = // code
EventStream<Boolean> transferValues = // code
combo = EventStreams.combine(hovers, controlPresses, shiftPresses, transferValues);
How would transferValues need to be constructed? Notice that it is also a "leaf."
// if using a SimpleBooleanProperty
SimpleBooleanProperty transferEnabled = new SimpleBooleanProperty(false);
EventStream<Boolean> transferValues = EventStreams.valuesOf(transferEnabled);
// if using a Val/Var
Var<Boolean> transferEnabled = Var.newSimpleVar(false);
EventStream<Boolean> transferValues = transferEnabled.values();
Now how about the controlPresses
and shiftPresses
EventStreams? How would they need to be constructed?
EventStream<KeyEvent> keyPressedOrReleased = // code
EventStream<Boolean> controlPresses = keyPressedOrReleased
.filter(key -> key.getCode().equals(KeyCode.CONTROL))
.map(key -> key.controlDown);
EventStream<Boolean> shiftPresses = keyPressedOrReleased
.filter(key -> key.getCode().equals(KeyCode.SHIFT))
.map(key -> key.controlDown);
The above code actually has a problem. Can you guess what it is?
Remember that when EventStreams.combine(args...)
is used, the returned EventStream<Tuple#<A, B, C...>>
cannot emit its event until all of the EventStreams upon which it relies emit at least one event. What would happen if the user started the program, hovered over the rectangle, and never pressed the control or shift keys? Absolutely NOTHING! combo
wouldn't emit an event, which means the fill of rectangle wouldn't be changed.
Thus, combo
won't emit an event until the user presses control and shift at least once. So let's fix it by further composing using withDefaultEvent
.
EventStream<KeyEvent> keyPressedOrReleased = // code
EventStream<Boolean> controlPresses = keyPressedOrReleased
.filter(key -> key.getCode().equals(KeyCode.CONTROL))
.map(key -> key.controlDown)
.withDefaultEvent(false);
EventStream<Boolean> shiftPresses = keyPressedOrReleased
.filter(key -> key.getCode().equals(KeyCode.SHIFT))
.map(key -> key.controlDown)
.withDefaultEvent(false);
Every recursive dependency has now been taken care of except for keyPressedOrReleased
. So, let's construct that:
EventStream<KeyEvent> keyPressed = EventStreams.eventsOf(scene, KeyEvent.KEY_PRESSED);
EventStream<KeyEvent> keyReleased = EventStreams.eventsOf(scene, KeyEvent.KEY_RELEASED);
EventStream<KeyEvent> keyPressedOrReleased = EventStreams.merge(keyPressed, keyReleased);
And there you have it! To write the code we rewrite everything from bottom to top. Let's see the full code now:
// set up keyPressedOrReleased
EventStream<KeyEvent> keyPressed = EventStreams.eventsOf(scene, KeyEvent.KEY_PRESSED);
EventStream<KeyEvent> keyReleased = EventStreams.eventsOf(scene, KeyEvent.KEY_RELEASED);
EventStream<KeyEvent> keyPressedOrReleased = EventStreams.merge(keyPressed, keyReleased);
// set up combo's dependencies
EventStream<Boolean> controlPresses = keyPressedOrReleased
.filter(key -> key.getCode().equals(KeyCode.CONTROL))
.map(key -> key.controlDown)
.withDefaultEvent(false);
EventStream<Boolean> shiftPresses = keyPressedOrReleased
.filter(key -> key.getCode().equals(KeyCode.SHIFT))
.map(key -> key.controlDown)
.withDefaultEvent(false);
EventStream<Boolean> hovers = EventStreams.valuesOf(rectangle.hoverProperty());
SimpleBooleanProperty transferEnabled = new SimpleBooleanProperty(false);
EventStream<Boolean> transferValues = EventStreams.valuesOf(transferEnabled);
// set up combo
EventStream<Tuple4<Boolean, Boolean, Boolean, Boolean> combo = EventStreams
.combine(hovers, controlPresses, shiftPresses, transferValues);
// map combo to Paint values, feed them to rectangle, and manage the returned Subscription:
Subscription filler = combo
.map(tuple4 -> {
boolean hovering = tuple4.get1();
boolean controlPressed = tuple4.get2();
boolean shiftPressed = tuple4.get3();
boolean transferEnabled = tuple4.get4();
if (!hovering && !controlPressed && !shiftPressed && !transferEnabled;) {
// scenario 1
return Color.RED;
} else if (hovering) {
if (!controlPressed && !shiftPressed && !transferEnabled;) {
// scenario 2
return Color.YELLOW;
} else if (controlPressed && !shiftPressed && !transferEnabled;) {
// scenario 3
return Color.WHITE;
} else if (!controlPressed && shiftPressed && transferEnabled;) {
// scenario 4
return Color.GRAY;
} else if (controlPressed && shiftPressedO {
if (transferEnabled) {
// scenario 5
return Color.PURPLE;
} else {
// scenario 6
return Color.MAROON;
}
}
}
})
.feedTo(rectangle.fillProperty());