0

I have a class with a collection of Seed elements. One of the method's return type of Seed is Optional<Pair<Boolean, String>>.

I'm trying to loop over all seeds, find if any boolean value is true and at the same time, create a set with all the String values. For instance, my input is in the form Optional<Pair<Boolean, String>>, the output should be Optional<Signal> where Signal is like:

class Signal {
   public boolean exposure;

   public Set<String> alarms;

   // constructor and getters (can add anything to this class, it's just a bag)
}

This is what I currently have that works:

// Seed::hadExposure yields Optional<Pair<Boolean, String>> where Pair have key/value or left/right
public Optional<Signal> withExposure() {
  if (seeds.stream().map(Seed::hadExposure).flatMap(Optional::stream).findAny().isEmpty()) {
    return Optional.empty();
  }
  final var exposure = seeds.stream()
      .map(Seed::hadExposure)
      .flatMap(Optional::stream)
      .anyMatch(Pair::getLeft);
  final var alarms = seeds.stream()
      .map(Seed::hadExposure)
      .flatMap(Optional::stream)
      .map(Pair::getRight)
      .filter(Objects::nonNull)
      .collect(Collectors.toSet());
  return Optional.of(new Signal(exposure, alarms));
}

Now I have time to make it better because Seed::hadExposure could become and expensive call, so I was trying to see if I could make all of this with only one pass. I've tried (some suggestions from previous questions) with reduce, using collectors (Collectors.collectingAndThen, Collectors.partitioningBy, etc.), but nothing so far.

x80486
  • 6,627
  • 5
  • 52
  • 111
  • what is the first `if` intended for? – Naman Sep 25 '20 at 03:49
  • 1
    You should stop overusing `Optional`, especially in this inconsistent way, i.e. returning an `Optional` that contains a `Pair` that may contain `null`. But if you want to keep this logic, you can do it with a single `public Optional withExposure() { return seeds.stream() .map(Seed::hadExposure) .flatMap(Optional::stream) .collect(teeing( mapping(Pair::getLeft, reducing(Boolean::logicalOr)), mapping(Pair::getRight, filtering(Objects::nonNull, toSet())), (o,set) -> o.map(b -> new Signal(b,set)))); }` But `teeing` needs JDK 12 or [a backport](https://stackoverflow.com/a/54030899/2711488). – Holger Sep 25 '20 at 11:31
  • I don't understand why do you say that. The `Optional` value wraps a `Pair`, what's inside that `Pair` could be what's inside any other object. I don't control the values coming from `Pair` but I do control what happens after it. – x80486 Sep 25 '20 at 12:18
  • 1
    The question is, *what does it mean* when the optional is empty, *what does it mean* when the string is null. It couldn’t be less obvious… – Holger Sep 25 '20 at 13:14

1 Answers1

1

It's possible to do this in a single stream() expression using map to convert the non-empty exposure to a Signal and then a reduce to combine the signals:

Signal signal = exposures.stream()
    .map(exposure ->
        new Signal(
            exposure.getLeft(),
            exposure.getRight() == null
                ? Collections.emptySet()
                : Collections.singleton(exposure.getRight())))
    .reduce(
        new Signal(false, new HashSet<>()),
        (leftSig, rightSig) -> {
            HashSet<String> alarms = new HashSet<>();
            alarms.addAll(leftSig.alarms);
            alarms.addAll(rightSig.alarms);
            return new Signal(
                leftSig.exposure || rightSig.exposure, alarms);
        });

However, if you have a lot of alarms it would be expensive because it creates a new Set and adds the new alarms to the accumulated alarms for each exposure in the input.

In a language that was designed from the ground-up to support functional programming, like Scala or Haskell, you'd have a Set data type that would let you efficiently create a new set that's identical to an existing set but with an added element, so there'd be no efficiency worries:

filteredSeeds.foldLeft((false, Set[String]())) { (result, exposure) => 
  (result._1 || exposure.getLeft, result._2 + exposure.getRight)
}

But Java doesn't come with anything like that out of the box.

You could create just a single Set for the result and mutate it in your stream's reduce expression, but some would regard that as poor style because you'd be mixing a functional paradigm (map/reduce over a stream) with a procedural one (mutating a set).

Personally, in Java, I'd just ditch the functional approach and use a for loop in this case. It'll be less code, more efficient, and IMO clearer.

If you have enough space to store an intermediate result, you could do something like:

List<Pair<Boolean, String>> exposures = 
    seeds.stream()
        .map(Seed::hadExposure)
        .flatMap(Optional::stream)
        .collect(Collectors.toList());

Then you'd only be calling the expensive Seed::hadExposure method once per item in the input list.

gabeg
  • 149
  • 2
  • A for loop solution equivalent to the code in question might look like following `var empty = true; var exposure = false; var alarms = new ArrayList<>(); for (Seed seed : seeds) { Optional> optionalPair = seed.hadExposure(); if (optionalPair.isPresent() && empty) { empty = false; if (optionalPair.get().getLeft()) { exposure = true; } String right = optionalPair.get().getRight(); if (right != null) { alarms.add(right); } } } return empty ? Optional.empty() : Optional.of(new Signal(exposure, alarms));` – Naman Sep 25 '20 at 03:53