On Fri, 1 Jul 2022 15:16:24 GMT, John Hendrikx <jhendr...@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 with a new target base due to a 
> merge or a rebase. The incremental webrev excludes the unrelated changes 
> brought in by the merge/rebase. The pull request contains 27 additional 
> commits since the last revision:
> 
>  - Merge branch 'openjdk:master' into feature/fluent-bindings
>  - Add null checks in Subscription
>  - Update copyrights
>  - Move private binding classes to com.sun.javafx.binding package
>  - Add note to Bindings#select to consider ObservableValue#flatMap
>  - Fix bug invalidation bug in FlatMappedBinding
>    
>    Also fixed a secondary issue where the indirect source of the binding
>    was unsubscribed and resubscribed each time its value was recomputed.
>    
>    Add additional comments to clarify how FlatMappedBinding works.
>    
>    Added test cases for these newly discovered issues.
>  - Fix typos in LazyObjectBinding
>  - Rename observeInputs to observeSources
>  - Expand flatMap javadoc with additional wording from Optional#flatMap
>  - Add since tags to all new API
>  - ... and 17 more: https://git.openjdk.org/jfx/compare/a0830f34...d66f2ba6

I didn't get into the fine details of the GC discussion yet, but I have a few 
points I would like to share:

1. @hjohn's example 4, which seems to be a point of some disagreement, is a _de 
facto_ issue. Even if the technicalities or semantics imply that this is 
correct behavior, StackOverflow is littered with "why did my listener suddenly 
stopped working?" questions that are caused by this, and I have fell into this 
pitfall more than once myself (by starting from something like example 2, then 
making a slight change that transforms the code into something like example 4).
  I didn't look yet into the possible alternatives discussed here for dealing 
with this, but I do believe that going with "this behavior is technically 
correct" is not the best _practical_ decision, provided that changing it will 
be backwards compatible.
2. On the subject of `Disposer` and cleaning references in a background thread, 
the JDK has 
[Cleaner](https://docs.oracle.com/en/java/javase/18/docs/api/java.base/java/lang/ref/Cleaner.html)
 (there is a nice recent Inside Java 
[post](https://inside.java/2022/05/25/clean-cleaner/) about it), which seems to 
do what we need, at least abstractly. I didn't look at the code to see if it 
fits our needs exactly. It also uses a `ReferenceQueue` with 
`PhantomReference`s.
3. It is clear to me that whatever GC/referencing model we choose to go with 
eventually (with whatever combination of fluent vs. regular bindings, listeners 
vs. bindings, expressions vs. properties...), we will have to document what is 
expected of the user, and we will have to do it very well. Currently, there are 
some mentions in the docs of listeners holding strong references, and weak 
references being used in the implementation ("user beware"), but we will need 
to be a lot more explicit in my opinion. Something like @hjohn's set of 
examples or some snippets from this discussion would be a good starting point. 
If we don't want to commit to some detail, we should at least document the 
current behavior with a note that it might change. It's just too easy to mess 
this up (even before this PR was considered). I don't mind taking care of such 
a PR when an implementation is agreed upon.

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

PR: https://git.openjdk.org/jfx/pull/675

Reply via email to