How to implement this nested flow with optionals?
Asked Answered
S

3

8

I've have a method that takes String as an input and should also return a String.

The following ASCII art presents the logical flow:

Option<A> optA = finder.findA(input);

          optA
           /\
isEmpty() /  \ isDefined()  
         /    \
 "ERR_1"       Option<B> optB = finder.findB(optA.get().bid);
                      / \
           isEmpty() /   \ isDefined()
                    /     \
                "ERR_2"    opt2.get().id

Basically for given input I'm looking for A object which is returned wrapped in an Option. Then is A is present I'm looking for B - wrapped in an Option too, otherwise return ERR_1. Then if B is present return it's id, otherwise return ERR_2.

I'm wondering how it could be implemented using optionals (or pattern matching maybe?) in a nice and concise way (without any ifology) - possibly in one-liner.

Could anyone please suggest something?

Source code to try out can be found here.

Sturges answered 17/11, 2016 at 16:5 Comment(3)
Optional<A> and Optional<B> don't relate to one another. Are you saying that the existence of Optional<A> depends on the existence of Optional<B>?Cavallaro
@Makoto: Optional<A> may exist even if Optional<B> does not exist.Sturges
I see it now; I was looking at the chart funny.Cavallaro
P
5

It looks like you have 3 possible exit points:

  1. optA empty -> "ERR_1"
  2. optA not empty && optB empty -> "ERR_2"
  3. both not empty -> optB.get().bid

You can achieve this by doing this with Javaslang:

 optA
   .map(a -> finder.findB(a.bid)
      .map(b -> b.bid)
      .getOrElse("ERR_2"))
   .getOrElse("ERR_1");

If optA is empty, we will jump straight to orElse("ERR_1")

If optA is not empty, we are using the value stored inside for getting value b.bid or "ERR_2" in case of optB emptiness.

Also, in pure Java 8, it would look like this:

optA
  .map(a -> finder.findB(a.bid)
    .map(b -> b.bid)
    .orElse("ERR_2"))
  .orElse("ERR_1");
Phrensy answered 17/11, 2016 at 16:34 Comment(4)
This is concerning; you can't return a different type to what the optional is specified to hold. That means that you can't use orElse to return "ERR_2" because it is a String.Cavallaro
@Cavallaro Check the code samples. ids are Strings too.Phrensy
That's fair. However, this still worries me; one is using stringly-typed errors as opposed to actual exceptions. Your answer may capture the verbatim code, but it violates that key principle of using a specific type for a specific purpose.Cavallaro
@Cavallaro I guess those are not real error codes, just some placeholder type for illustrating the flow betterPhrensy
O
4

Since you're using javaslang, Try seems to be a better choice because it propagates the error throught the chain, while Option only propagates its "emptiness".

If you can change findA and findB to return Try you get:

Try<B> b = finder.findA(input)
    .flatMap(a -> finder.findB(a.bid))

If you can't, then:

Try<B> b = finder.findA(input).toTry(() -> new Exception("ERR_1"))
    .flatMap(a -> findB(a.bId).toTry(() -> new Exception("ERR_2")))

That gets a tentative B, I'm unsure if you want to collapse the valid value and the error into the same value, if that's the case then:

String value = b.getOrElseGet(Throwable::getMessage)

If you have an issue with creating pointless exceptions, you can use an Either in each of the find operations, where the left value is your error type. That seems to model the problem better, but may have the downside of longer type signatures, depending on how you split up the expressions.

Ossuary answered 17/11, 2016 at 22:3 Comment(0)
C
0

You'll want to adapt this to your code, but this is an approach I'd use.

First, isolate the errors you want to encapsulate for each point of failure.

Supplier<? extends RuntimeException> missingAException = IllegalStateException::new;
Supplier<? extends RuntimeException? missingBException = IllegalStateException::new;

Doing this allows you to write a lambda later on to provide a specific error message if you so desire.

Now, we write the optionals.

Optional<A> optA = finder.find(input);
Optional<B> optB = finder.findB(optA.orElseThrow(missingAException));

To extract optB, use the same pattern as we did for optA.

B value = optB.orElseThrow(missingBException);
Cavallaro answered 17/11, 2016 at 16:23 Comment(7)
It's much easier to do it with exceptions which I'd like to avoid.Sturges
@Opal: Do you have a sentinel value you'd like to provide to the result instead? Otherwise, you're back to testing for existence with if statements.Cavallaro
what do you mean exactly? I suppose I've no such value.Sturges
@Opal: What I asked you is if you have a value that you can return other than null if finder.find came back without anything. You mention "ERR_1" and others as strings, but those come across to me as exceptions (since you can't return String with your Optional).Cavallaro
@Cavallaro instead of Exceptions, it would be better to utilize Try if we are already using JavaslangPhrensy
@Makoto, there's no such value.Sturges
@pivovarit: I've not had any experience with Javaslang and didn't realize that this could be an approach. I was using a more vanilla Java 8 approach.Cavallaro

© 2022 - 2024 — McMap. All rights reserved.