JEP Proposal: Fluent Bindings for multiple observables

John Hendrikx john.hendrikx at gmail.com
Tue Oct 28 23:32:10 UTC 2025


>
>     It is an extension on the fluent API, it doesn't introduce
>     anything that isn't possible currently, just like `map` and
>     `flatMap` didn't introduce anything new, nor did `subscribe`.  It
>     is intended to be a more discoverable, fluent and modern API, and
>     to fill a gap where one has to go from the fluent binding model to
>     a static helper class model as soon as you go from mapping just
>     one source to needing two or more sources.
>
>
>  The 'select` methods on Bindings were not compile-time safe and IIRC
> used reflection. I never used them. Subscribe made memory management
> easier and avoided sneaky memory leaks (which can still happen, just
> less often). There were very good reasons to include them. Here it's
> more a matter of ergonomics.

True, perhaps that isn't enough to justify them.  They do allow some
other interesting things perhaps later that may be useful.

First, you could subscribe on a combination of properties:

       x.with(y).map(Point::new).subscribe(p ->  ... get points here ... );

To make this easy to use, we'd want to make `map` null safe, like `map`
in `ObservableValue`, and you'll just have to use `orElse` to deal with
`null`s (or perhaps a `mapNull` variant I've seen in some stream type
frameworks).

Then I realized there could be another potentially cool use here for the
`null` skipping behavior (but I'll admit it is a bit far fetched, but
perhaps it will lead to other ideas).  What if I did this:

       // initially (x, y) are (0, 10):
       x.with(y).map(Point::new).subscribe(System.out::println);
       x.set(null);
       y.set(null);
       x.set(10);
       y.set(0);

Because how `map` would skip nulls, this would only print Point(0, 10)
and Point(10, 0) ...

As said, it is far fetched, and clumsy to do, but I've thinking for a
long time about adding some sort of transactional logic for properties
as well (and I do have some other solutions for that already that I've
been playing with) and this kind of mini-transaction sort of popped up
out of nowhere.

--John

>
> On Tue, Oct 28, 2025 at 10:28 AM John Hendrikx
> <john.hendrikx at gmail.com> wrote:
>
>     Thanks for taking a look Nir, I really appreciate it :)
>
>     On 26/10/2025 13:56, Nir Lisker wrote:
>>     When I need to combine observable values I usually do something like:
>>
>>     Bindings.createDoubleBinding(() ->  width.get() / height.get(),
>>     height, width);
>>
>>     which is much less cumbersome than subclassing (although a bit
>>     less performant I think). It works for an arbitrary number of
>>     observables (including) observable lists/sets/maps too:
>
>     You're right, I forgot about the existence of the Bindings class,
>     and the helpers it has added for these cases.  I barely use it
>     since the addition of fluent bindings.  That version is a lot more
>     compact and a bit more workable.
>
>>
>>     Bindings.createDoubleBinding(() ->  list.getFirst() /
>>     height.get() * map.get("A"), height, list, map); // assume this
>>     makes sense somehow
>>
>>     ReactFX, which is the go-to library for such extension, has a
>>     'combine' method that work like this:
>>
>>     Val.combine(height, width, (h, w) -> w / h);
>
>     I'm aware of this, and it is something to be considered as an
>     addition.  It is similar to the argument where there is a
>     `Subscription.combine` and `subscription.and`, one being static
>     and the other being a fluent method.  I think however we shouldn't
>     be relying on ReactFX too much anymore these days (I haven't used
>     it in years now).  The primary reason for that is that ReactFX had
>     to introduce new property classes which are limited in how well
>     they can interop with existing JavaFX code.  With the fluent
>     bindings additions the need for ReactFX is limited.  That is not
>     to say ReactFX doesn't offer anything interesting anymore :)
>
>>     To assess the proposal, I tried to write your JEP examples with
>>     the current JavaFX.
>>
>>     Multi-stage chain (not sure how you mapped to a Point3D from a
>>     Point2D and a number):
>>
>>     ObjectBinding<Point2D> point2d = Bindings.createObjectBinding(()
>>     -> new Point2D(x.get(), y.get()), x, y);
>>     ObjectBinding<Point3D> point3d = Bindings.createObjectBinding(()
>>     -> new Point3D(point2d.get().getX(), point2d.get().getY(),
>>     z.get()), point2d, z);
>>
>>     Combining chains:
>>
>>
>>     ObjectBinding<Point2D> point1 = Bindings.createObjectBinding(()
>>     -> new Point2D(x.get(), y.get()), x, y);
>>
>>     ObjectBinding<Point2D> point2 = Bindings.createObjectBinding(()
>>     -> new Point2D(x.get(), y.get()), x, y);
>>     ObjectBinding<Line2D> line = Bindings.createObjectBinding(() ->
>>     new Line2D(point1, point2), point1, point2); // Line2D is not a
>>     JavaFX class
>>
>>
>>     Using a default value:
>>
>>     ObservableValue<Point2D> point1 = Bindings.createObjectBinding(()
>>     -> new Point2D(x.get(), y.get()), x, y).orElse(Point2D.ZERO);
>>
>>
>>     Some observations:
>>     Bindings returns a Binding rather than an ObservableValue and
>>     also has primitive specialization versions that have their unique
>>     methods (add, subtract...). These methods, however, can be
>>     replicated with fluent bindings and they also have the "known"
>>     subtle GC issue for the intermediary values.
>     Yes, I don't think we should cater to primitive specializations.
>     Bindings tend to be high level enough (often eventually tied to a
>     UI) that it makes little sense to "optimize" these at the cost of
>     8x more variants. The GC issues are often insidious, and my
>     problems in that area have largely disappeared with the addition
>     of the fluent bindings and subscribe API's.  This is why I'm
>     hesitant to use API's from the Bindings class, and why I think FX
>     should offer alternatives in that area.
>>     Also, static imports can make these calls less ceremonious:
>>     createObjectBinding(() -> new Point2D(x.get(), y.get()), x, y);
>     Static imports are not really a plus for any argument in my view
>     :)  Compare AssertJ and Hamcrest to see what I mean.
>>
>>     The proposal is more ergonomic with its fluency for a couple of
>>     values, but I'm not sure it solves enough problems that the
>>     current mechanism can't.
>
>     It is an extension on the fluent API, it doesn't introduce
>     anything that isn't possible currently, just like `map` and
>     `flatMap` didn't introduce anything new, nor did `subscribe`.  It
>     is intended to be a more discoverable, fluent and modern API, and
>     to fill a gap where one has to go from the fluent binding model to
>     a static helper class model as soon as you go from mapping just
>     one source to needing two or more sources.
>
>     --John
>
>>
>>     On Sun, Oct 26, 2025 at 11:59 AM John Hendrikx
>>     <john.hendrikx at gmail.com> wrote:
>>
>>         JEP:
>>         https://gist.github.com/hjohn/611acb65769b68a845b8919c62a3e99a
>>
>>         Hi everyone,
>>
>>         I'd like to propose an extension to the fluent bindings API on
>>         ObservableValue (map, flatMap, orElse) which were introduced
>>         in JavaFX
>>         19 over 3 years ago.
>>
>>         The API currently is very powerful when dealing with a single
>>         observable, but lacks support when dealing with multiple
>>         observables. 
>>         For example, let's say you want to compute a width/height
>>         ratio.  You
>>         could write this:
>>
>>             ObservableValue<Double> ratio = width.map(w -> w /
>>         height.get());
>>
>>         ... but you'll quickly find that such an observable will not
>>         update
>>         itself when height changes, only when width changes.
>>
>>         The go-to solution for this is currently:
>>
>>             DoubleBinding ratio = new DoubleBinding() {
>>                 { bind(width, height); }
>>
>>                 protected double computeValue() { return width.get() /
>>         height.get(); }
>>             }
>>
>>         My proposal would extend ObservableValue with a new `with`
>>         method that
>>         returns an intermediate stage that can be easily converted
>>         back to an
>>         ObservableValue:
>>
>>             ObservableValue<Double> ratio =
>>         width.with(height).map((w, h) -> w /
>>         h);  // yields a ratio that updates whenever w or h changes
>>
>>         Or for example:
>>
>>              ObservableValue<Point> point =
>>         x.with(y).map(Point::new);  //
>>         yields a Point that updates whenever x or y changes
>>
>>         The intermediate stage would not be an observable value
>>         itself.  This
>>         limits the API surface, and makes this proposal fairly
>>         lightweight and
>>         much easier to implement.
>>
>>         Please see the JEP for the full proposal.  I look forward to
>>         your feedback!
>>
>>         --John
>>
>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/openjfx-dev/attachments/20251029/0e0686d6/attachment-0001.htm>


More information about the openjfx-dev mailing list