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/cacb6847...d66f2ba6

> I'm not entirely in agreement here, but also not entirely disagreeing. I 
> mean, if there was a way to make this work perfectly, then I agree that this 
> "automatic" unsubscribing is a great mechanism. However, because you are 
> relying on the GC the code is actually subtly broken right from the start -- 
> you just don't notice it so easily:
> 
> 1. [...]
> 2. [...]
> 3. The issues I highlighted in my earlier post. Why is there a difference in 
> how listeners work when it is a property vs an expression? Why does it work 
> differently when using a binding? Replacing a binding with a listener and 
> vice versa can have surprising consequences.

I think a model with the following properties would be more consistent and 
easier to understand than that we currently have:
1. An `ObservableValue` is eligible for collection if it is not reachable from 
user code, independent of whether it has listeners.
2. A derived `ObservableValue` keeps its base value alive, i.e. the base value 
is reachable through the derived value.

The second property ensures that it will be sufficient to keep a reference to 
the last `ObservableValue` in an expressions like 
`caption.map(String::toUpperCase).map(String::toLowerCase()` without losing any 
of the intermediate values.

It is true that manually added listeners may suffer from unexpected collection, 
but something has to give. It seems like a reasonable trade-off to make 
listeners more complicated than binding expressions (which also hopefully 
discourages the use of manually added listeners). I would also argue that it is 
easier to explain that it's not the _listener_ that's different in properties 
v. expressions, it's the _reachability_ of the `ObservableValue` that's 
different.

> Just to make sure we are on same page, it leaks a bit more memory than it 
> would if it was using `concat` or a regular property, as the listener stub is 
> leaked + the mapping itself. The label is not leaked. The leak "fixes" itself 
> (in both cases) once caption is updated: the left over listener stub which 
> `label` added gets removed, which in turn causes an unsubscribe on the 
> mapping, which in turn unsubscribes itself from `caption`. The mapping is 
> then eligible for GC.

Thanks for this clarification, I wasn't aware that the leaked listener is 
actually removed when the source property is changed. That makes the issue a 
little bit less concerning, but I'd still prefer a solution that doesn't 
require the source property to change.

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

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

Reply via email to