Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to chain lambdas with all optional values available at the innermost scope without nesting Optional#ifPresent()?

This is an offshoot of my other question: How to chain Optional#ifPresent() in lambda without nesting?

However, the problem now is how to provide a lambda solution where all of the optional values are available at the innermost scope:

B b = procA().flatMap(this::procB).orElseThrow(SomeException::new);

// Value from procA() is not available.

My original code was:

void SomeMethod() {
    procA().ifPresent(a -> {
       procB(a).ifPresent(b -> {
          // Do something with a and b

          return;
       });
    });

    throw new SomeException();
}

I understand that the return at the innermost scope is wrong. The new flatMap example illustrates the correct behavior.

I am using ifPresent() instead of get() to avoid potential runtime exceptions where I might fail to check whether the value of an optional isPresent().

like image 263
Zhro Avatar asked Nov 07 '22 15:11

Zhro


1 Answers

I find this question very interesting as chained calls with potential null returns are a common nuisance, and Optional can shorten the usual null check chain a lot. But the issue there is that the nature of the functional stream methods hides the intermediate values in the mapping functions. Nesting is a way to keep them available, but can also get annoying if the length of the call chain grows, as you have realized.

I cannot think of an easy and lightweight solution, but if the nature of your project leads to these situations regularly, this util class could help:

public static class ChainedOptional<T>
{
    private final List<Object> intermediates;

    private final Optional<T>  delegate;

    private ChainedOptional(List<Object> previousValues, Optional<T> delegate)
    {
        this.intermediates = new ArrayList<>(previousValues);
        intermediates.add(delegate.orElse(null));
        this.delegate = delegate;
    }

    public static <T> ChainedOptional<T> of(T value)
    {
        return of(Optional.ofNullable(value));
    }

    public static <T> ChainedOptional<T> of(Optional<T> delegate)
    {
        return new ChainedOptional<>(new ArrayList<>(), delegate);
    }

    public <R> ChainedOptional<R> map(Function<T, R> mapper)
    {
        return new ChainedOptional<>(intermediates, delegate.map(mapper));
    }

    public ChainedOptional<T> ifPresent(Consumer<T> consumer)
    {
        delegate.ifPresent(consumer);
        return this;
    }

    public ChainedOptional<T> ifPresent(BiConsumer<List<Object>, T> consumer)
    {
        delegate.ifPresent(value -> consumer.accept(intermediates, value));
        return this;
    }

    public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier)
        throws X
    {
        return delegate.orElseThrow(exceptionSupplier);
    }

    public <X extends Throwable> T orElseThrow(Function<List<Object>, X> exceptionSupplier)
        throws X
    {
        return orElseThrow(() -> exceptionSupplier.apply(intermediates));
    }
}

You use it by wrapping an Optional or a plain value. When you then use the map method to chain method calls, it will provide a new ChainedOptional while storing the current value in a list. At the end (ifPresent, orElseThrow), you will not only get the last value, but also the list of all intermediate values. Since it is not known how many calls will be chained, I did not find a way to store those values in a type-safe way, though.

See examples here:

ChainedOptional.of(1)
               .map(s -> s + 1)
               .map(s -> "hello world")
               .map(s -> (String) null)
               .map(String::length)
               .ifPresent((intermediates, result) -> {
                   System.out.println(intermediates);
                   System.out.println("Result: " + result);
               })
               .orElseThrow(intermediates -> {
                   System.err.println(intermediates);
                   return new NoSuchElementException();
               });

// [1, 2, hello world, null, null]
// Exception in thread "main" java.util.NoSuchElementException
//    at ... 

ChainedOptional.of(1)
               .map(s -> s + 1)
               .map(s -> "hello world")
               // .map(s -> (String) null)
               .map(String::length)
               .ifPresent((intermediates, result) -> {
                   System.out.println(intermediates);
                   System.out.println("Result: " + result);
               })
               .orElseThrow(intermediates -> {
                   System.err.println(intermediates);
                   return new NoSuchElementException();
               });

// [1, 2, hello world, 11]
// Result: 11

Hope this helps. Let me know if you come up with a nicer solution.

like image 146
Malte Hartwig Avatar answered Nov 15 '22 13:11

Malte Hartwig