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