> > 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 > <[email protected]> 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 >> <[email protected]> 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 >> >>
