Search code examples
javafunctional-programmingjava-8vavr

Finding the "most" correct value using Java 8 Predicate


and thanks for checking out my question!

I'm having a little trouble wrapping my head around using Streams with multiple predicates being applied in specific order.

For the sake of example, please consider the following IntPredicates:

        IntPredicate divisibleByThree = i -> i % 3 == 0;
        IntPredicate divisibleByFour = i -> i % 4 == 0;
        IntPredicate divisibleByFive = i -> i % 5 == 0;
        IntPredicate divisibleByThreeAndFour = divisibleByThree.and(divisibleByFour);
        IntPredicate divisibleByThreeAndFive = divisibleByThree.and(divisibleByFive);
        IntPredicate divisibleByThreeAndFiveAndFour = divisibleByThreeAndFour.and(divisibleByFive);
        //....arbitrary Number of predicates.

Part 1

I have converted the problem I have down to a "FizzBuzz"-esque version, trying to find the correct answer by applying the predicates to a stream in a specific order. Like so:

    IntStream.range(1, 100).forEach(i -> {
        //Order matters here!
        if(divisibleByThreeAndFiveAndFour.test(i)){
            System.out.println("Three and four and five");
        } else if(divisibleByThreeAndFour.test(i)){
            System.out.println("Three and four");
        } else if(divisibleByThreeAndFive.test(i)){
            System.out.println("Three and four");
        } else if(divisibleByFive.test(i)){
            System.out.println("Five");
        }
        //etc, etc.
    });

I don't think this is very pretty code, is there a better way of achieving this?

Part 2

How about if I really need to apply the predicates to see if an appropriate value is present in the stream, and computing a related value to be returned(in this case, a string to print). How would that even look?

A proposed naive solution:

String bestValueFound = "None found";
if(IntStream.range(1, 100).filter(divisibleByThreeAndFiveAndFour).findFirst().isPresent()){
    bestValueFound = "Three and four and five";
} else if(IntStream.range(1, 100).filter(divisibleByThreeAndFour).findFirst().isPresent()){
    bestValueFound = "Three and four";
}else if(IntStream.range(1, 100).filter(divisibleByThreeAndFive).findFirst().isPresent()){
    bestValueFound = "Three and five";
} else if(IntStream.range(1, 100).filter(divisibleByThreeAndFive).findFirst().isPresent()){
    bestValueFound = "Five";
}
System.out.println(bestValueFound);

This seems even worse, both aesthetically and because of the added iterations.

Part 3

Could this possibly be solved in a prettier, more efficient way using JavaSlang Match?

//Note: Predicates needed to be changed from IntPredicate to Predicate<Integer> for correct compilation.
Function<Integer, String> findString = i -> API.Match(i).of(
        Case(divisibleByThreeAndFiveAndFour, "Three and four and five"),
        Case(divisibleByThreeAndFour, "Three and four"),
        Case(divisibleByThreeAndFive, "Three and five"),
        Case(divisibleByFive, "Fice"),
        Case($(), "None found"));
String bestValueFound =  IntStream.range(1, 100).boxed().map(findString).findFirst().orElseThrow(() -> new RuntimeException("Something went wrong?"));
System.out.println(bestValueFound);

The obvious problem here is the ".findFirst()", which would be the Integer 1 on this case, making the entire expression evaluating to "None found", and then terminating.

What I want is to essentially grab anything that matches the very first predicate in my match, and use that value, if it exists, and only give me whatever matches the second Case if no matches for the first one are found, and so on, only giving me the default("None found"), if no value in the stream matches any predicate.

There has to be a better way of doing this, right? Or am I just wasting my time trying to do something that is just better left to a more traditional, imperative style?

Thank you for reading my question!


Solution

  • You can create a class to encapsulate the predicate and its name:

    class NamedPredicate {
        final String name;
        final IntPredicate predicate;
    
        NamedPredicate(String name, IntPredicate predicate) {
            this.name = name;
            this.predicate = predicate;
        }
    
        NamedPredicate and(NamedPredicate other) {
            return new NamedPredicate(this.name + " and " + other.name,
                    this.predicate.and(other.predicate));
        }
    }
    

    The and() method allows us to compose them similar to the way you did originally:

    NamedPredicate divisibleByThree = new NamedPredicate("three", i -> i % 3 == 0);
    NamedPredicate divisibleByFour = new NamedPredicate("four", i -> i % 4 == 0);
    NamedPredicate divisibleByFive = new NamedPredicate("five", i -> i % 5 == 0);
    NamedPredicate divisibleByThreeAndFour = divisibleByThree.and(divisibleByFour);
    NamedPredicate divisibleByThreeAndFive = divisibleByThree.and(divisibleByFive);
    NamedPredicate divisibleByThreeAndFiveAndFour = divisibleByThreeAndFour.and(divisibleByFive);
    

    Now we can stream through them in descending order and print the name of the first matching predicate, for each i:

    IntStream.range(1, 100)
            .mapToObj(i -> Stream.of(
                        divisibleByThreeAndFiveAndFour,
                        divisibleByThreeAndFour,
                        divisibleByThreeAndFive,
                        divisibleByFive,
                        divisibleByFour,
                        divisibleByThree)
                    .filter(p -> p.predicate.test(i))
                    .findFirst()
                    .map(p -> p.name)
                    .orElse("none"))
            .forEach(System.out::println);