Skip to content

How to Compose an EventStream Well

Clément Fournier edited this page Oct 1, 2017 · 7 revisions

Guidelines

The Process

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:

  1. 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?

  2. 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".

  3. 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.

Other Helpful Tips

  • 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.

Some Examples

Example 1 - Changing a Rectangle's Fill Color

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?

Example 1's Dependencies: The Naive Approach

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);

Example 1's Dependencies: The Less Boilerplate Approach

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());

EventStream Dependencies: Composing combo

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);

Example 1's Full Code

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());
Clone this wiki locally