Java Enums as Predicates

Introduction

This article examines extending Java Enums used as property values within JavaBeans combined with the Java Stream API to create and extend fluent interfaces. From Wikipedia:

In software engineering, a fluent interface … is a method for designing object oriented APIs based extensively on method chaining with the goal of making the readability of the source code close to that of ordinary written prose, essentially creating a domain-specific language within the interface.

The Java Stream API provides the method chaining; this article will examine how Java Enums may be extended (specifically to implement Predicate) to contribute to a fluent interface’s quality of being similar to “written prose.” For example:

public enum Rank implements Predicate<Card> {
    JOKER, ACE, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING;
    ...
}

public enum Suit implements Predicate<Card> {
    CLUBS, DIAMONDS, HEARTS, SPADES;
    ...
}

public class Card {
    private final Suit suit;
    private final Rank rank;
    ...
}

will allow support for fluent expressions like:

    List<Card> hand = ...;

    boolean areAllHearts =
        hand.stream()
        .filter(Suit.HEARTS)
        .allMatch();

    boolean hasKingOfSpades =
        hand.stream()
        .filter(Rank.KING.and(Suit.SPADES))
        .anyMatch();

Complete javadoc is provided.

Extending Enums

Enum values are constants but they are also subclasses of Enum and those subclass implementations may have custom fields and methods. For example, java.time.DayOfWeek implements the TemporalAccessor and TemporalAdjuster interfaces so DayOfWeek provides implementation methods for those interface methods. The Suit implementation demonstrates how subclass fields may be defined and set by defining a custom constructor.

    public enum Suit {
        CLUBS(Color.BLACK, "\u2667"),
        DIAMONDS(Color.RED, "\u2662"),
        HEARTS(Color.RED, "\u2661"),
        SPADES(Color.BLACK, "\u2664");
        ...
        private final Color color;
        private final String string;

        @ConstructorProperties({ "color", EMPTY })
        private Suit(Color color, String string) {
            this.color = color;
            this.string = string;
        }

        public Color getColor() { return color; }

        @Override
        public String toString() { return string; }
        ...
    }

Implementing Predicate

The key to contributing to the fluent interface provided by Stream is for the Enum subclass to implement Predicate. Of course, that Predicate must test the bean that the Enum is a property for. For example, Rank and Suit must test Card:

    public enum Rank implements Predicate<Card> {
        ...
        @Override
        public boolean test(Card card) {
            return is(this).test(card.getRank());
        }
        ...
        public static Predicate<Rank> is(Rank rank) {
            return t -> Objects.equals(rank, t);
        }
        ...
    }

    public enum Suit implements Predicate<Card> {
        ...
        @Override
        public boolean test(Card card) {
            return is(this).test(card.getSuit());
        }
        ...
        public static Predicate<Suit> is(Suit rank) {
            return t -> Objects.equals(rank, t);
        }
        ...
    }

Note: Both implementations provide static is methods to further contribute to the “fluency” of the API.

To re-inforce the fact that Rank and Suit are bean properties of Card, Rank and Suit are implemented as inner classes of Card.

Fluent Implementation - Poker Hand Ranking

To demonstrate the “fluency” of the API, a Poker Ranking Enum may be defined.

public enum Ranking implements Predicate<List<Card>> {
    Empty(0, Collection::isEmpty),
        HighCard(1, t -> true),
        Pair(2, Rank.SAME),
        TwoPair(4, Pair.with(Pair)),
        ThreeOfAKind(3, Rank.SAME),
        Straight(5, Rank.SEQUENCE),
        Flush(5, Suit.SAME),
        FullHouse(5, ThreeOfAKind.with(Pair)),
        FourOfAKind(4, Rank.SAME),
        StraightFlush(5, holding(ACE, KING).negate().and(Straight).and(Flush)),
        RoyalFlush(5, holding(ACE, KING).and(Straight).and(Flush)),
        FiveOfAKind(5, Rank.SAME);

    private final int required;
    private final Predicate<List<Card>> is;

    private Ranking(int required, Predicate<List<Card>> is) {
        this.required = required;
        this.is = Objects.requireNonNull(is);
    }
    ...
    public int required() { return required; }
    ...
    @Override
    public boolean test(List<Card> list) {
        return (list.size() >= required()
                && is.test(subListTo(list, required())));
    }
    ...
}

To complete the Poker “domain specific language” the Rank and Suit types must provide static Predicate SAME fields,

    ...
    private static <T> Predicate<List<T>> same(Function<T,Predicate<T>> mapper) {
        return t -> ((! t.isEmpty()) && t.stream().allMatch(mapper.apply(t.get(0))));
    }
    ...
    public enum Rank implements Predicate<Card> {
        ...
        public static final Predicate<List<Card>> SAME = same(Card::getRank);
        ...
    }
    ...
    public enum Suit implements Predicate<Card> {
        ...
        public static final Predicate<List<Card>> SAME = same(Card::getSuit);
        ...
    }
    ...

Rank must provide a static SEQUENCE Predicate,

    ...
    private static <T,R> List<R> listOf(Collection<T> collection,
                                        Function<T,R> mapper) {
        return collection.stream().map(mapper).collect(Collectors.toList());
    }
    ...
    public enum Rank implements Predicate<Card> {
        ...
        public static final List<Rank> ACE_HIGH  =
            unmodifiableList(asList(JOKER,
                                    TWO, THREE, FOUR, FIVE,
                                    SIX, SEVEN, EIGHT, NINE,
                                    TEN, JACK, QUEEN, KING, ACE));
        public static final List<Rank> ACE_LOW =
            unmodifiableList(asList(values()));

        private static final Map<String,Rank> MAP;
        private static final List<List<Rank>> SEQUENCES;

        static {
            TreeMap<String,Rank> map =
                new TreeMap<>(String.CASE_INSENSITIVE_ORDER);

            for (Rank rank : values()) {
                map.put(rank.name(), rank);
                map.put(rank.toString(), rank);
            }

            MAP = unmodifiableMap(map);

            List<Rank> high = new ArrayList<>(Rank.ACE_HIGH);
            List<Rank> low = new ArrayList<>(Rank.ACE_LOW);

            reverse(high);
            reverse(low);

            SEQUENCES =
                unmodifiableList(asList(unmodifiableList(high),
                                        unmodifiableList(low)));
        }
        ...
        public static final Predicate<List<Card>> SEQUENCE =
            t -> ((! t.isEmpty()) && sequence(listOf(t, Card::getRank)));

        private static boolean sequence(List<Rank> list) {
            return (SEQUENCES.stream().anyMatch(t -> indexOfSubList(t, list) >= 0));
        }
        ...
    }
    ...

and Ranking must provide static with and holding methods:

public enum Ranking implements Predicate<List<Card>> {
    ...
    private Predicate<List<Card>> with(Predicate<List<Card>> that) {
        return t -> test(t) && that.test(subListFrom(t, required()));
    }

    private static <T> Predicate<List<T>> holding(int count, Predicate<List<T>> predicate) {
        return t -> (t.isEmpty() || predicate.test(subListTo(t, count)));
    }

    @SafeVarargs
    @SuppressWarnings({ "varargs" })
    private static <T> Predicate<List<T>> holding(Predicate<T>... array) {
        return holding(Stream.of(array).collect(Collectors.toList()));
    }

    private static <T> Predicate<List<T>> holding(List<Predicate<T>> list) {
        return t -> ((list.isEmpty() || t.isEmpty())
                     || (list.get(0).test(t.get(0))
                         && (holding(subListFrom(list, 1)).test(subListFrom(t, 1)))));
    }

    private static <T> List<T> subListTo(List<T> list, int to) {
        return list.subList(0, Math.min(to, list.size()));
    }

    private static <T> List<T> subListFrom(List<T> list, int from) {
        return list.subList(from, list.size());
    }
    ...
}

The Ranking Predicate Enum combined with the Combinations Stream introduced in this article may be used to test for a specific Poker hand.

    List<Card> hand = ...;
    int size = Math.min(5, hand.size());

    boolean isStraight =
        Combinations.of(size, hand)
        .filter(Ranking.STRAIGHT)
        .anyMatch();

While this implementation is complete, the Combinations Stream provides an of(int,int,Predicate<List<T>>,Collection<T>) method that allows the specification of a Predicate that when it evaluates to false will stop iterating over that branch. The Ranking Enum may be extended to provide that Predicate by providing a possible() method:

public enum Ranking implements Predicate<List<Card>> {
    Empty(0, null, Collection::isEmpty),
        HighCard(1, t -> true, t -> true),
        Pair(2, Rank.SAME, Rank.SAME),
        TwoPair(4, holding(2, Rank.SAME), Pair.with(Pair)),
        ThreeOfAKind(3, Rank.SAME, Rank.SAME),
        Straight(5, Rank.SEQUENCE, Rank.SEQUENCE),
        Flush(5, Suit.SAME, Suit.SAME),
        FullHouse(5, holding(3, Rank.SAME), ThreeOfAKind.with(Pair)),
        FourOfAKind(4, Rank.SAME, Rank.SAME),
        StraightFlush(5,
                      holding(ACE, KING).negate().and(Rank.SEQUENCE).and(Suit.SAME),
                      holding(ACE, KING).negate().and(Straight).and(Flush)),
        RoyalFlush(5,
                   holding(ACE, KING).and(Rank.SEQUENCE).and(Suit.SAME),
                   holding(ACE, KING).and(Straight).and(Flush)),
        FiveOfAKind(5, Rank.SAME, Rank.SAME);

    private final int required;
    private final Predicate<List<Card>> possible;
    private final Predicate<List<Card>> is;

    private Ranking(int required, Predicate<List<Card>> possible, Predicate<List<Card>> is) {
        this.required = required;
        this.possible = possible;
        this.is = Objects.requireNonNull(is);
    }
    ...
    public Predicate<List<Card>> possible() {
        return t -> (possible == null || possible.test(subListTo(t, required())));
    }
    ...
}

which in combination with the Combinations.of(int,int,Predicate<List<T>>,Collection<T>) method will optimize the search for ThreeOfAKind by escaping a branch if the first Cards are not the same Rank, Straight if the first Cards are not a sequence, etc.

    List<Card> hand = ...;
    int size = Math.min(5, hand.size());

    boolean isStraight =
        Combinations.of(size, size, STRAIGHT.possible(), hand)
        .filter(Ranking.STRAIGHT)
        .anyMatch();

The logic in Ranking.find(Collection<Card>) and Evaluator demonstrate more sophisticated logic.

Summary

Implementing Predicate(BEAN) for BEAN property types (including Enum) will contribute to making an API “fluent” when used in combination with Stream.