ObservableValue.map special-cases the null value

John Hendrikx john.hendrikx at gmail.com
Thu May 18 23:31:45 UTC 2023


On 18/05/2023 19:05, Michael Strauß wrote:
> Hi John,
>
> the `isPresent()` and `isEmtpy()` methods don't feel entirely natural
> to me. I think the problem is that Optional has slightly different
> semantics compared to ObservableValue. An Optional either has a value,
> or it doesn't. The "it doesn't have a value" state doesn't feel
> exactly equivalent to "having the null value", since no Optional API
> will ever expose the null value. In contrast, ObservableValue will
> happily expose the null value.

ObservableValue is more of an Optional that is mutable (but is presented 
read only). It's certainly not a perfect match, but most methods that 
make sense for Optional also make good sense for ObservableValue. The 
main difference is that some methods of Optional return a final result 
(which is fine because it is immutable), while the same method for 
ObservableValue must return another ObservableValue (as it can change at 
any time, being mutable):

       optional.orElse(x)  -->  fixed value
       optional.map(...) --> another Optional

Vs:

       observableValue.orElse(x)  -->  another ObservableValue
       observableValue.map(...)  -->  another ObservableValue

Optional's isPresent and isEmpty return booleans; in the observable 
world, where the value can change, it makes sense that the return values 
themselves would be observables.  Other methods we may "steal" that I've 
considered (but not found a good use case for yet) are:

      or  --  can be useful as it is hard/impossible to do with current 
primitives
      filter  --  "filter(x -> x == 2)" would be equivalent to "map(x -> 
x == 2 ? x : null)" but more convenient
      orElseGet  -- a dynamic variant of orElse: orElseGet(() -> 
getResourceBundle(locale).get("empty.text"))

Methods that make no sense for a mutable Optional are:

       ifPresent -- would have to be called "ifCurrentlyPresent"... 
makes no sense
       ifPresentOrElse  -- a combination of map/orElse (and a very ugly 
method with 2 lambda's)
       orElseThrow  -- seems pointless, this would be better as a 
getValue alternative (getValueOrThrowIfNull)
       stream -- would have to be a hot stream, which the Stream API 
doesn't do (cold streams only AFAIK)


> While the map(...).orElse(...) combination is sufficient for some
> scenarios, it doesn't allow to disambiguate the case where map()
> returns `null` from the case where map() was elided because the input
> argument was `null`. That's where mapNullable() would be materially
> different. But I acknowledge that this is an edge case.

Optional does this as well, you can't tell if it was already empty or if 
it has become empty due to mapping to null:

       assertEquals( Optional.empty(), Optional.ofNullable(x).map(x -> 
null) )

What would be the use case for wanting to know if the current value was 
null already or if your mapping function made it null (or perhaps the 
next mapping function did it?) It seems to me that if you want to know 
if it is currently null, you can just call `getValue`.  If you don't 
know what your mapping function is doing (ie, it may sometimes return 
null) that seems like a problem that need not be solved by ObservableValue.

--John


>
> On Wed, May 17, 2023 at 3:08 AM John Hendrikx <john.hendrikx at gmail.com> wrote:
>> Hi Michael,
>>
>> As you're only trying to map to true/false, it is a bit ugly to do this
>> I suppose:
>>
>>         nullableValue.map(x -> true).orElse(false);
>>
>> I've encountered situations like these before, where you only want to
>> know about the presence of a value, and don't care what the value is.
>> If there are sufficient use cases, we could consider adding a method for
>> this.  Instead of `mapNullable` though I was thinking more along the
>> lines of `isPresent` and `isEmpty`:
>>
>>         ObservableValue<Boolean> isPresent();  // true if not null
>>
>>         ObservableValue<Boolean> isEmpty();  // true if null
>>
>> So far I didn't think it would be warranted to add such methods, but
>> they're trivial to add.
>>
>> On 17/05/2023 00:25, Michael Strauß wrote:
>>> I'm trying to map an ObservableValue such that `null` is mapped to
>>> `false` and any other value is mapped to `true`. The following line of
>>> code should do the trick:
>>> ObservableValue<Boolean> mappedValue = nullableValue.map(x -> x != null);
>>>
>>> It turns out that this doesn't work, since `map` doesn't pass `null`
>>> to the mapping function. Instead of calling the mapping function, it
>>> always returns `null`. This is not technically a bug, as `map` is
>>> specified to have this behavior. It is a bit unfortunate, though;
>>> `null` is a perfectly valid value in many cases and its special
>>> treatment comes off as a bit of a surprise.
>> The alternative is worse I think, as JavaFX properties can easily be
>> `null`, you'd have to add null checks to your map/flatMaps. Especially a
>> construct like this:
>>
>>         node.sceneProperty()
>>             .flatMap(Scene::windowProperty)
>>             .flatMap(Window::showingProperty)
>>             .orElse(false);
>>
>> ... would be really annoying to write if you had to deal with the
>> nulls.  The functionality is closely related to what Optional offers,
>> and that one doesn't pass `null` to its map/flatMap methods either, nor
>> will it ever add a `mapNullable` :-)
>>
>>> I'm wondering whether we need a `mapNullable` operator that does what
>>> I intuitively expecting `map` to do, which is just unconditionally
>>> passing the value to the mapping function.
>> Maybe, I think however that you'd only need this for the very special
>> case where you are intending to ignore the actual value being passed to
>> a map/flatMap, and only want to check it for null.  `isPresent` /
>> `isEmpty` would be nicer for this.  For all other cases, where you are
>> actually doing some kind of mapping, I think a map/orElse combo works great.
>>
>> --John
>>


More information about the openjfx-dev mailing list