Functional composition of Optionals
Asked Answered
M

7

10

I have 2 Optionals (or Maybe objects) that I would like to combine so that I get the following results:

                ||       first operand 
    second      ++-------------+-------------
    operand     ||    empty    | optional(x)
    ============||=============|=============
    empty       ||    empty    | optional(x)
    ------------++-------------+-------------
    optional(y) || optional(y) |optional(x+y)

In other words, a non-empty Optional always replaces/overwrites an empty one, and two non-empty Optionals are combined according to some + function.

Initially, I assumed that the standard monadic flatMap method would do the trick, but (at least in Java) Optional.flatMap always returns an empty optional when the original Optional was already empty (and I'm not sure if any other implementation would comply with the Monad Laws).

Then, as both operands are wrapped in the same monadic type, I figured that this might be a good job for an Applicative Functor. I tried a couple different functional libraries, but I couldn't implement the desired behavior with any of the zip/ap methods that I tried.

What I'm trying to do seems to me a fairly common operation that one might do with Optionals, and I realize that I could just write my own operator with the desired behavior. Still, I am wondering if there is a standard function/method in functional programming to achieve this common operation?

Update: I removed the java tag, as I'm curious how other languages handle this situation

Morpheme answered 17/6, 2022 at 20:38 Comment(6)
I've not kept up with Java after 9 but I don't see anything in the documentation that looks like ap to me. I suppose you can jury-rig it with Stream.concat(a.stream(), b.stream()) which will give you a stream that's empty or has 1 or 2 items. You can then operate with it and collect it back to an Optional. But it still feels like hack.Iminourea
Why not look at JVM-based functional languages instead (such as Scala or Clojure)?Privative
Thanks @Iminourea - using streams is a decent work-around, but I agree that it feels somewhat hacky. I was hoping that such a straightforward problem would have a simple solution.Morpheme
@Privative I'm open to considering other JVM-based languages. Does Scala's or Clojure's library have a standard function that achieves what I'm looking for?Morpheme
Honestly I don't know, but Java is not a functional language (you never hear the word "monadic" when talking about Java). So I guess you will have better luck with some real functional languages.Privative
In haskell, this would be liftU2 (+)Peay
D
4

In Haskell you can do this by wrapping any semigroup in a Maybe. Specifically, if you want to add numbers together:

Prelude> import Data.Semigroup
Prelude Data.Semigroup> Just (Sum 1) <> Just (Sum 2)
Just (Sum {getSum = 3})
Prelude Data.Semigroup> Nothing <> Just (Sum 2)
Just (Sum {getSum = 2})
Prelude Data.Semigroup> Just (Sum 1) <> Nothing
Just (Sum {getSum = 1})
Prelude Data.Semigroup> Nothing <> Nothing
Nothing

The above linked article contains more explanations, and also some C# examples.

Dysphoria answered 18/6, 2022 at 0:32 Comment(1)
This is a really interesting solution. Part of my difficulty with the original problem was that there is a lot of boilerplate code for dealing with all the empty/non-empty combination, and the real "meat" of the problem is how to combine two optionals that are non-empty. The wrapping in a semigroup seems like a nice way to convey which function to use for the combination, and it does not require passing an additional function objectMorpheme
C
6

In a functional language, you'd do this with pattern matching, such as (Haskell):

combine :: Maybe t -> Maybe t -> (t -> t -> t) -> Maybe t
combine (Some x) (Some y) f = Some (f x y)
combine (Some x) _ _ = (Some x)
combine _ (Some y) _ = (Some y)
combine None None _ = None

There are other ways to write it, but you are basically pattern matching on the cases. Note that this still involves "unpacking" the optionals, but because its built into the language, it is less obvious.

Chrysalis answered 17/6, 2022 at 21:30 Comment(0)
N
4

It's not possible to combine optional objects without "unpacking" them.

I don't know the specifics of your case. For me, creating such a logic just in order to fuse the two optionals is an overkill.

But nevertheless, there's a possible solution with streams.

I assume that you're not going to pass optional objects as arguments (because such practice is discouraged). Therefore, there are two dummy methods returning Optional<T>.

Method combine() expects a BinaryOperator<T> as an argument and creates a stream by concatenating singleton-streams produced from each of the optional objects returned by getX() and getY().

The flavor of reduce(BinaryOperator) will produce an optional result.

public static <T> Optional<T> getX(Class<T> t) {
    return // something
}

public static <T> Optional<T> getY(Class<T> t) {
    return // something
}

public static <T> Optional<T> combine(BinaryOperator<T> combiner, 
                                      Class<T> t) {
    
    return Stream.concat(getX(t).stream(), getY(t).stream())
        .reduce(combiner);
}

If we generalize the problem to "how to combine N optional objects" then it can be solved like this:

@SafeVarargs
public static <T> Optional<T> combine(BinaryOperator<T> combiner,
                                      Supplier<Optional<T>>... suppliers) {
    
    return Arrays.stream(suppliers)
        .map(Supplier::get)           // fetching Optional<T>
        .filter(Optional::isPresent)  // filtering optionals that contain results to avoid NoSuchElementException while invoking `get()`
        .map(Optional::get)           // "unpacking" optionals
        .reduce(combiner);
}
Neritic answered 17/6, 2022 at 20:57 Comment(1)
If there's something incorrect, maybe you can spend a minute to describe the issue?Neritic
T
4

Here's one way:

a.map(x -> b.map(y -> x + y).orElse(x)).or(() -> b)

Ideone Demo

Tribasic answered 17/6, 2022 at 21:8 Comment(2)
This looks quite ugly but it does do what OP wants. I wish Java provided something more straight forward than having to jump through such hoops.Iminourea
Nice! That's a very concise way of implementing this with just Optionals (as opposed to Streams). I agree with @Iminourea that I was hoping to have this already implemented in a standard method somewhere.Morpheme
D
4

In Haskell you can do this by wrapping any semigroup in a Maybe. Specifically, if you want to add numbers together:

Prelude> import Data.Semigroup
Prelude Data.Semigroup> Just (Sum 1) <> Just (Sum 2)
Just (Sum {getSum = 3})
Prelude Data.Semigroup> Nothing <> Just (Sum 2)
Just (Sum {getSum = 2})
Prelude Data.Semigroup> Just (Sum 1) <> Nothing
Just (Sum {getSum = 1})
Prelude Data.Semigroup> Nothing <> Nothing
Nothing

The above linked article contains more explanations, and also some C# examples.

Dysphoria answered 18/6, 2022 at 0:32 Comment(1)
This is a really interesting solution. Part of my difficulty with the original problem was that there is a lot of boilerplate code for dealing with all the empty/non-empty combination, and the real "meat" of the problem is how to combine two optionals that are non-empty. The wrapping in a semigroup seems like a nice way to convey which function to use for the combination, and it does not require passing an additional function objectMorpheme
T
2
OptionalInt x = ...
OptionalInt y = ...

OptionalInt sum = IntStream.concat(x.stream(), y.stream())
    .reduce(OptionalInt.empty(),
        (opt, z) -> OptionalInt.of(z + opt.orElse(0)));

Since java 9 you can turn an Optional into a Stream. With concat you get a Stream of 0, 1 or 2 elements.

Reduce it to an empty when 0 elements,and for more add it to the previous OptionalInt, defaulting to 0.

Not very straight (.sum()) because of the need for an empty().

Tillotson answered 17/6, 2022 at 21:42 Comment(0)
L
2

You can implement your function in Java by combining flatMap and map:

optA.flatMap(a -> optB.map(b -> a + b));

More general example:

public static void main(String[] args) {
    test(Optional.empty(), Optional.empty());
    test(Optional.of(3), Optional.empty());
    test(Optional.empty(), Optional.of(4));
    test(Optional.of(3), Optional.of(4));
}

static void test(Optional<Integer> optX, Optional<Integer> optY) {
    final Optional<Integer> optSum = apply(Integer::sum, optX, optY);
    System.out.println(optX + " + " + optY + " = " + optSum);
}

static <A, B, C> Optional<C> apply(BiFunction<A, B, C> fAB, Optional<A> optA, Optional<B> optB) {
    return optA.flatMap(a -> optB.map(b -> fAB.apply(a, b)));
}

Since flatMap and map are standard functions for Optional/Maybe (and monad types generally), this approach should work in any other language (though most FP languages will have a more concise solution). E.g. in Haskell:

combine ma mb = do a <- ma ; b <- mb ;  return (a + b)
Lil answered 28/6, 2022 at 10:42 Comment(0)
S
1

In F#, i would call this logic reduce.

Reason:

  • The function must be of type 'a -> 'a -> 'a as it only can combine thinks of equal type.

  • Like other reduce operations, like on list, you always need at least one value, otherwise it fails.

With a option and two of them, you just need to cover four cases. In F# it will be written this way.

(* Signature: ('a -> 'a -> 'a) -> option<'a> -> option<'a> -> option<'a> *)
let reduce fn x y =
    match x,y with
    | Some x, Some y -> Some (fn x y)
    | Some x, None   -> Some x
    | None  , Some y -> Some y
    | None  , None   -> None

printfn "%A" (reduce (+) (Some 3) (Some 7)) // Some 10
printfn "%A" (reduce (+) (None)   (Some 7)) // Some 7
printfn "%A" (reduce (+) (Some 3) (None))   // Some 3
printfn "%A" (reduce (+) (None)   (None))   // None

In another lets say Pseudo-like C# language, it would look like.

Option<A> Reduce(Action<A,A,A> fn, Option<A> x, Option<A> y) {
    if ( x.isSome ) {
        if ( y.isSome ) {
            return Option.Some(fn(x.Value, y.Value));
        }
        else {
            return x;
        }
    }
    else {
        if ( y.isSome ) {
            return y;
        }
        else {
            return Option.None;
        }
    }
}
Semitropical answered 19/6, 2022 at 20:16 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.