Proof of concept for fluent bindings for ObservableValue

Nir Lisker nlisker at gmail.com
Wed Apr 7 01:41:33 UTC 2021


>
> In the PoC I made I specifically also disallowed 'null' as an input
>

I like the way ReactFX does it where the property is empty. I think that
this is also what you mean by disallowing `null` (in other contexts,
"disallowing null" would mean throwing an exception).

Not entirely sure what you mean by this.
>

Basically, what you said. My point was that this is a different API
section. The first deals with expanding the observables/properties methods.
The second with listeners methods. Even if mapping a property requires a
new listening model, like subscriptions, this is done under the hood.
Exposing this API should be a separate step. At least that's how I see it.

I'd be happy to spend more time and work on this. Perhaps it would be
> possible to collaborate on this?
>

That would be good. I will need to re-review the ReactFX internals and see
how your proposal differs exactly.

 By the way, do you make a distinction between ReactFX's Val and Var in
your proposal (one being read-only)?

On Sun, Apr 4, 2021 at 12:43 PM John Hendrikx <hjohn at xs4all.nl> wrote:

>
>
> On 02/04/2021 08:47, Nir Lisker wrote:
> > Hi John,
> >
> > I've had my eyes set on ReactFX enhancements for a while too, especially
> as
> > a replacement for the unsafe "select" mechanism. One of the things that
> > kept me from going forward with this is seeing what Valhalla will bring.
> > Generic specialization might save a lot of duplication work on something
> > like this, and Tomas touched another related issue [1], but since it
> could
> > be a long time before that happens, it's worth planning what we can
> extract
> > from ReactFX currently.
>
> Agreed, Valhalla is certainly a highly anticipated feature but I fear it
> is still a couple of years away.
>
> Even without any initial support for dealing with "? extends Number"
> from the various ObservableValue specializations I think looking into
> this can already be tremendous help.
>
> The proof of concept mainly requires you convert the Number to a
> suitable type when reading the property but has no problems in the other
> direction:
>
>      label.widthProperty().map(Number::doubleValue).map(x -> x + 1);
>
> Not pretty, but certainly workable. Specific methods could be introduced
> (even at a later time) to make this more streamlined, similar to what
> the Stream API offers with 'mapToDouble' etc.
>
> > I think that we should break the enhancements into parts.
> > The first that I would advise to look at are the additions to
> > properties/observables. Tomas had to create Val and Var because he
> couldn't
> > change the core interfaces, but we can. Fitting them with the Optional
> > methods like `isPresent`, `isEmpty`, `ifPresent`, `map`. `flatMap` etc.;
> > and `select` and friends, is already a good start that will address many
> > common requirements.
>
> Yes, Val/Var had to be created for that reason, and also because
> properties don't quite behave the same as streams -- streams with a
> "toBinding" method results in things people didn't quite expect.
>
> As far as the Optional methods go, I'm not entirely sure properties
> would benefit from all of them. Properties are not immutable like
> Optional and it may make less sense to fit them with 'isPresent',
> 'isEmpty' and 'ifPresent' ('ifPresent' would I think need to behave
> similar to 'addListener' or 'subscribe').
>
> In the PoC I made I specifically also disallowed 'null' as an input for
> functions like 'map' and 'flatMap' (opting to use 'orElse' semantics for
> 'null'), as this for allows much cleaner mapping (and especially flat
> mapping when selecting nested properties). If 'null' were to be allowed,
> I think at a minimum we'd need to add another method to allow for easy
> selecting of nested properties to avoid:
>
>      obs.flatMap(x -> x == null ? null : x.otherProperty())
>
> > The second part is related to listeners. The subscription model and event
> > streams try to solve the memory issues with hard and weak references, and
> > allow better composition.
>
> Not entirely sure what you mean by this. JavaFX's current model uses
> weak references which was I think an unfortunate decision as it can
> result in huge confusion.  For example, a direct binding will work, but
> with an indirection step a binding stops working:
>
>      button.textProperty()
>         .concat("World")  // weak binding used here
>         .addListener((obs, old, cur) -> System.out.println(cur));
>
> The above stops working, but without the 'concat' it keeps working.
>
> I think the use of weak listeners should be avoided and instead other
> mechanisms should be provided to make cleaning up easier. This is the
> main reason for 'conditionOn' and why ReactFX even had a specialized
> version of it: 'conditionOnShowing(Node)'.
>
> > The third part is for collections - things like transformation lists
> > (LiveList) and for other collections.
>
> This is indeed best saved for last. The problems there I think are less
> of an issue for now.
>
> > Since these share behavior under the hood, we need to look ahead, but in
> > terms of functionality, I think we should take smaller steps. It will
> also
> > be easier to propose these then.
>
> I've for this reason kept the PoC small with only the most basic
> functionality.  I did however add some work for a different subscription
> model, mainly because the internals of this code benefits greatly from
> it. It is however kept to a minimum.
>
> I'd be happy to spend more time and work on this. Perhaps it would be
> possible to collaborate on this?
>
> --John
>
> >
> > - Nir
> >
> > [1]
> >
> https://github.com/TomasMikula/ReactFX/wiki/Creating-a-Val-or-Var-Instance#the-javafx-propertynumber-implementation-issue
> >
> > On Wed, Mar 24, 2021 at 11:49 PM John Hendrikx <hjohn at xs4all.nl> wrote:
> >
> >> I just wanted to draw some attention to a recent proof of concept I made
> >> in this pull request: https://github.com/openjdk/jfx/pull/434
> >>
> >> It is based on the work I did in
> >> https://github.com/hjohn/hs.jfx.eventstream which is in part based on
> >> work done in ReactFX by Tomas Mikula. The PR itself however shares no
> >> code with ReactFX and is
> >> completely written by me.
> >>
> >> If there is interest, I'm willing to invest more time in smoothing out
> >> the API and documentation, investigating further how this would interact
> >> with the primitive types and adding unit test coverage (I have extensive
> >> tests, but thesea are written in JUnit 5, so they would require
> >> conversion or JavaFX could move to support JUnit 5).
> >>
> >> What follows below is the text of the PR for easy reading. Feedback is
> >> appreciated.
> >>
> >> ================
> >>
> >> This is a proof of concept of how fluent bindings could be introduced to
> >> JavaFX. The main benefit of fluent bindings are ease of use, type safety
> >> and less surprises. Features:
> >>
> >> 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: #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 or orElseGet.
> >>
> >> Conditional Bindings
> >> Bindings can be made conditional using the conditionOn method. A
> >> conditional binding retains its last value when its condition is false.
> >> Conditional bindings donot observe their source when the condition is
> >> false, allowing developers to automatically stop listening to properties
> >> when a certain condition is met. A major use of this feature is to have
> >> UI components that need to keep models updated which may outlive the UI
> >> conditionally update the long lived model only when the UI is showing.
> >>
> >> 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
> >>    ));
> >>
> >>    // 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")
> >>    );
> >> }
> >>
> >> void updateLongLivedModelWhileAvoidingMemoryLeaks() {
> >>    // Standard JavaFX: naive, memory leak; UI won't get garbage
> collected
> >>    listView.getSelectionModel().selectedItemProperty().addListener(
> >>      (obs, old, current) ->
> >> longLivedModel.lastSelectedProperty().set(current)
> >>    );
> >>
> >>    // Standard JavaFX: no leak, but stops updating after a while
> >>    listView.getSelectionModel().selectedItemProperty().addListener(
> >>      new WeakChangeListener<>(
> >>        (obs, old, current) ->
> >> longLivedModel.lastSelectedProperty().set(current)
> >>      )
> >>    );
> >>
> >>    // Standard JavaFX: fixed version
> >>    listenerReference = (obs, old, current) ->
> >> longLivedModel.lastSelectedProperty().set(current);
> >>
> >>    listView.getSelectionModel().selectedItemProperty().addListener(
> >>      new WeakChangeListener<>(listenerReference)
> >>    );
> >>
> >>    // Fluent: naive, memory leak... fluent won't solve this...
> >>    listView.getSelectionModel().selectedItemProperty()
> >>        .subscribe(longLivedModel.lastSelectedProperty()::set);
> >>
> >>    // Fluent: conditional update when control visible
> >>
> >>    // Create a property which is only true when the UI is visible:
> >>    ObservableValue<Boolean> showing = listView.sceneProperty()
> >>        .flatMap(Scene::windowProperty)
> >>        .flatMap(Window::showingProperty)
> >>        .orElse(false);
> >>
> >>    // Use showing property to automatically disconnect long lived model
> >>    // allowing garbage collection of the UI:
> >>    listView.getSelectionModel().selectedItemProperty()
> >>      .conditionOn(showing)
> >>      .subscribe(longLivedModel.lastSelectedProperty()::set);
> >>
> >>    // Note that the 'showing' property can be provided in multiple ways:
> >>    // - create manually (can be re-used for multiple bindings though)
> >>    // - create with a helper: Nodes.showing(Node node) ->
> >> ObservableValue<Boolean>
> >>    // - make it part of the Node class; as the fluent bindings only bind
> >> themselves
> >>    //   to their source when needed (lazy binding), this won't create
> >> overhead
> >>    //   for each node in the scene
> >> }
> >> 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
> >>
> >
>


More information about the openjfx-dev mailing list