RFR: 8274771: Map, FlatMap and OrElse fluent bindings for ObservableValue [v14]

Kevin Rushforth kcr at openjdk.java.net
Fri May 27 23:37:53 UTC 2022


On Tue, 22 Mar 2022 07:46:40 GMT, John Hendrikx <jhendrikx at openjdk.org> wrote:

>> This is an implementation of the proposal in https://bugs.openjdk.java.net/browse/JDK-8274771 that me and Nir Lisker (@nlisker) have been working on.  It's a complete implementation including good test coverage.  
>> 
>> This was based on https://github.com/openjdk/jfx/pull/434 but with a smaller API footprint.  Compared to the PoC this is lacking public API for subscriptions, and does not include `orElseGet` or the `conditionOn` conditional mapping.
>> 
>> **Flexible Mappings**
>> Map the contents of a property any way you like with `map`, or map nested properties with `flatMap`.
>> 
>> **Lazy**
>> The bindings created are lazy, which means they are always _invalid_ when not themselves observed. This allows for easier garbage collection (once the last observer is removed, a chain of bindings will stop observing their parents) and less listener management when dealing with nested properties.  Furthermore, this allows inclusion of such bindings in classes such as `Node` without listeners being created when the binding itself is not used (this would allow for the inclusion of a `treeShowingProperty` in `Node` without creating excessive listeners, see this fix I did in an earlier PR: https://github.com/openjdk/jfx/pull/185)
>> 
>> **Null Safe**
>> The `map` and `flatMap` methods are skipped, similar to `java.util.Optional` when the value they would be mapping is `null`. This makes mapping nested properties with `flatMap` trivial as the `null` case does not need to be taken into account in a chain like this: `node.sceneProperty().flatMap(Scene::windowProperty).flatMap(Window::showingProperty)`.  Instead a default can be provided with `orElse`.
>> 
>> Some examples:
>> 
>>     void mapProperty() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(Bindings.createStringBinding(() -> text.getValueSafe().toUpperCase(), text));
>> 
>>       // Fluent: much more compact, no need to handle null
>>       label.textProperty().bind(text.map(String::toUpperCase));
>>     }
>> 
>>     void calculateCharactersLeft() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(text.length().negate().add(100).asString().concat(" characters left"));
>> 
>>       // Fluent: slightly more compact and more clear (no negate needed)
>>       label.textProperty().bind(text.orElse("").map(v -> 100 - v.length() + " characters left"));
>>     }
>> 
>>     void mapNestedValue() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(Bindings.createStringBinding(
>>         () -> employee.get() == null ? ""
>>             : employee.get().getCompany() == null ? ""
>>             : employee.get().getCompany().getName(),
>>         employee
>>       ));
>> 
>>       // Standard JavaFX + Optional:
>>       label.textProperty().bind(Bindings.createStringBinding(
>>           () -> Optinal.ofNullable(employee.get())
>>               .map(Employee::getCompany)
>>               .map(Company::getName)
>>               .orElse(""),
>>          employee
>>       ));
>> 
>>       // Fluent: no need to handle nulls everywhere
>>       label.textProperty().bind(
>>         employee.map(Employee::getCompany)
>>                 .map(Company::getName)
>>                 .orElse("")
>>       );
>>     }
>> 
>>     void mapNestedProperty() {
>>       // Standard JavaFX:
>>       label.textProperty().bind(
>>         Bindings.when(Bindings.selectBoolean(label.sceneProperty(), "window", "showing"))
>>           .then("Visible")
>>           .otherwise("Not Visible")
>>       );
>> 
>>       // Fluent: type safe
>>       label.textProperty().bind(label.sceneProperty()
>>         .flatMap(Scene::windowProperty)
>>         .flatMap(Window::showingProperty)
>>         .orElse(false)
>>         .map(showing -> showing ? "Visible" : "Not Visible")
>>       );
>>     }
>> 
>> Note that this is based on ideas in ReactFX and my own experiments in https://github.com/hjohn/hs.jfx.eventstream.  I've come to the conclusion that this is much better directly integrated into JavaFX, and I'm hoping this proof of concept will be able to move such an effort forward.
>
> John Hendrikx has updated the pull request incrementally with one additional commit since the last revision:
> 
>   Fix wording

I reviewed the public API changes, and this looks like a great addition to JavaFX bindings. I think there might be time to get this into JavaFX 19, presuming that there are no issues with the testing or implementation, so let's proceed down that path.

I left one comment on the API docs as well as pointed out the public methods that will need an `@since 19` javadoc tag.

Once that is updated you can propagate the javadoc changes to the CSR (including the `@since` tags) and move it to "Proposed". I'll formally review it later, once the code review is closer to being done.

modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java line 198:

> 196:      * @return {@code true} if this binding currently has one or more
> 197:      *     listeners registered on it, otherwise {@code false}
> 198:      */

This needs an `@since` tag. Presuming that this makes JavaFX 19, that would be:


     * @since 19

modules/javafx.base/src/main/java/javafx/beans/binding/ObjectBinding.java line 213:

> 211:      *
> 212:      * @return {@code true} if this binding is allowed to become valid, otherwise
> 213:      *     {@code false}

Needs an `@since` tag.

modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java line 166:

> 164:      *     mapping function on this value, or {@code null} when it
> 165:      *     is {@code null}; never returns {@code null}
> 166:      * @throws NullPointerException if the mapping function is {@code null}

Needs an `@since` tag.

modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java line 192:

> 190:      *     holds {@code null}; can be {@code null}
> 191:      * @return an {@code ObservableValue} that holds this value, or the given constant if
> 192:      *     it is {@code null}; never returns {@code null}

Needs an `@since` tag.

modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java line 205:

> 203:      * resulting value is {@code null}. If the mapping resulted in {@code null}, then the
> 204:      * resulting value is also {@code null}.
> 205:      * <p>

It might be worth borrowing some language from `Optional::flatMap`. Maybe something like this?


This method is similar to {@link #map(Function)}, but the mapping function is
one whose result is already an ObservableValue, and if invoked, flatMap does
not wrap it within an additional ObservableValue.

modules/javafx.base/src/main/java/javafx/beans/value/ObservableValue.java line 239:

> 237:      *     {@code null} when the value is {@code null}; never returns {@code null}
> 238:      * @throws NullPointerException if the mapping function is {@code null}
> 239:      */

This also needs an `@since` tag.

-------------

PR: https://git.openjdk.java.net/jfx/pull/675


More information about the openjfx-dev mailing list