Saturday 6 July 2013

Pattern Matching (Java - using generics)

The purpose of this page is to document a pattern matching class (which uses generics) as an accompaniment to the blog Using TotallyLazy the functional library for Java. To view a simpler version of the class (without generics) please go to Pattern Matching (Java - non-generic).

I use pattern matching a lot in functional languages and find it very natural and readable (to the initiated). Therefore, when I come back to imperative languages such as Java, I find wading through the many nested branching 'if' statements quite horrible and antiquated. For this reason I decided to create a pattern matching class in Java.

Here is an example of how to use the pattern matching class. At the end the 'matchResult' string will equal "43b" because the input parameters match the "with(_, 'b')" method. The underscore character '_' is used as a wildcard.

import com.googlecode.totallylazy.Function2;
import static net.intrepidis.library.functional.PatternMatch._;
import static net.intrepidis.library.functional.PatternMatch.match;
class Example {
    void example() {
        String matchResult =
            match(3, 'b').returns(String.class)
            .with(1, 'a').then(returnWith("1"))
            .with(2, 'b').then(returnWith("2"))
            .with(1, 'b').then(returnWith("3"))
            .with(_, 'b').then(returnWith("4"))
            .with(1, _).then(returnWith("5"))
            .with(_, _).then(returnWith("6"))
            .result();
    }

    private static Function2<Integer, Character, String> returnWith(final String position) {
        return new Function2<Integer, Character, String>() {
            public String call(Integer p1, Character p2) {
                return position + p1 + p2;
            }};
    }
}

Here's another example that uses TotallyLazy to create a function which counts how many vowels are in a string. This is a complicated example and reading more about the TotallyLazy library would help with understanding it.

import com.googlecode.totallylazy.Function1;
import com.googlecode.totallylazy.Callable2;
import static net.intrepidis.library.functional.PatternMatch._;
import static net.intrepidis.library.functional.PatternMatch.match;
class Example {
    void example() {
        Function1<String, Integer[]> countVowels = new Function1<String, Integer[]>() {
            @Override
            public Integer[] call(String str) {
                return characters(str)
                    .fold(new Integer[]{0, 0, 0, 0, 0},
                        new Callable2<Integer[], Character, Integer[]>() {
                            @Override
                            public Integer[] call(final Integer[] acc, Character letter) throws Exception {
                                Integer indexToIncrement =
                                    match(letter).returns(Integer.class)
                                        .with('a').then(0)
                                        .with('e').then(1)
                                        .with('i').then(2)
                                        .with('o').then(3)
                                        .with('u').then(4)
                                        .result();
                                if (indexToIncrement != null)
                                    acc[indexToIncrement]++;
                                return acc;
                            }
                    });
            }
        };
        
        test(numbers(countVowels.call("The quick brown fox jumps over the lazy dog"))).is(1, 3, 1, 4, 2);
    }
}

Below is the full listing of the PatternMatch class:

package net.intrepidis.library.functional;

import com.googlecode.totallylazy.Function1;
import com.googlecode.totallylazy.Function2;
import com.googlecode.totallylazy.Function3;
import com.googlecode.totallylazy.Function4;
import com.googlecode.totallylazy.Function5;
import com.googlecode.totallylazy.Pair;
import com.googlecode.totallylazy.Callable1;

import static com.googlecode.totallylazy.Sequences.sequence;

public class PatternMatch<TR, T1, T2, T3, T4, T5> {
    private boolean matched = false;
    private TR result;
    private final Object[] parameters;

    // Wildcard object matches anything.
    public static final Wildcard _ = new Wildcard();

    private PatternMatch(Object... parameters) {
        this.parameters = parameters;
    }

    public static <U1> PatternMatch<Object, U1, Object, Object, Object, Object> match(U1 p1) {
        return new PatternMatch<Object, U1, Object, Object, Object, Object>(p1);
    }

    public static <U1, U2> PatternMatch<Object, U1, U2, Object, Object, Object> match(U1 p1, U2 p2) {
        return new PatternMatch<Object, U1, U2, Object, Object, Object>(p1, p2);
    }

    public static <U1, U2, U3> PatternMatch<Object, U1, U2, U3, Object, Object> match(U1 p1, U2 p2, U3 p3) {
        return new PatternMatch<Object, U1, U2, U3, Object, Object>(p1, p2, p3);
    }

    public static <U1, U2, U3, U4> PatternMatch<Object, U1, U2, U3, U4, Object> match(U1 p1, U2 p2, U3 p3, U1 p4) {
        return new PatternMatch<Object, U1, U2, U3, U4, Object>(p1, p2, p3, p4);
    }

    public static <U1, U2, U3, U4, U5> PatternMatch<Object, U1, U2, U3, U4, U5> match(U1 p1, U2 p2, U3 p3, U1 p4, U2 p5) {
        return new PatternMatch<Object, U1, U2, U3, U4, U5>(p1, p2, p3, p4, p5);
    }

    @SuppressWarnings("UnusedParameters")
    public <UR> PatternMatch<UR, T1, T2, T3, T4, T5> returns(Class<UR> returnType) {
        return new PatternMatch<UR, T1, T2, T3, T4, T5>(parameters);
    }

    public With1 with(Object p1) {
        return new With1(haveMatch(p1));
    }

    public With2 with(Object p1, Object p2) {
        return new With2(haveMatch(p1, p2));
    }

    public With3 with(Object p1, Object p2, Object p3) {
        return new With3(haveMatch(p1, p2, p3));
    }

    public With4 with(Object p1, Object p2, Object p3, Object p4) {
        return new With4(haveMatch(p1, p2, p3, p4));
    }

    public With5 with(Object p1, Object p2, Object p3, Object p4, Object p5) {
        return new With5(haveMatch(p1, p2, p3, p4, p5));
    }

    public abstract class WithBase {
        protected boolean act = false;

        private WithBase(boolean act) {
            this.act = act;
        }

        public PatternMatch<TR, T1, T2, T3, T4, T5> then(TR returnValue) {
            if (act) {
                result = returnValue;
            }
            return PatternMatch.this;
        }
    }

    public class With1 extends WithBase {
        private With1(boolean act) {
            super(act);
        }

        @SuppressWarnings("unchecked")
        public PatternMatch<TR, T1, T2, T3, T4, T5> then(Function1<T1, TR> actor) throws Exception {
            if (act) {
                result = actor.call(
                        (T1)parameters[0]);
            }
            return PatternMatch.this;
        }
    }

    public class With2 extends WithBase {
        private With2(boolean act) {
            super(act);
        }

        @SuppressWarnings("unchecked")
        public PatternMatch<TR, T1, T2, T3, T4, T5> then(Function2<T1, T2, TR> actor) throws Exception {
            if (act) {
                result = actor.call(
                        (T1)parameters[0],
                        (T2)parameters[1]);
            }
            return PatternMatch.this;
        }
    }

    public class With3 extends WithBase {
        private With3(boolean act) {
            super(act);
        }

        @SuppressWarnings("unchecked")
        public PatternMatch<TR, T1, T2, T3, T4, T5> then(Function3<T1, T2, T3, TR> actor) throws Exception {
            if (act) {
                result = actor.call(
                        (T1)parameters[0],
                        (T2)parameters[1],
                        (T3)parameters[2]);
            }
            return PatternMatch.this;
        }
    }

    public class With4 extends WithBase {
        private With4(boolean act) {
            super(act);
        }

        @SuppressWarnings("unchecked")
        public PatternMatch<TR, T1, T2, T3, T4, T5> then(Function4<T1, T2, T3, T4, TR> actor) throws Exception {
            if (act) {
                result = actor.call(
                        (T1)parameters[0],
                        (T2)parameters[1],
                        (T3)parameters[2],
                        (T4)parameters[3]);
            }
            return PatternMatch.this;
        }
    }

    public class With5 extends WithBase {
        private With5(boolean act) {
            super(act);
        }

        @SuppressWarnings("unchecked")
        public PatternMatch<TR, T1, T2, T3, T4, T5> then(Function5<T1, T2, T3, T4, T5, TR> actor) throws Exception {
            if (act) {
                result = actor.call(
                        (T1)parameters[0],
                        (T2)parameters[1],
                        (T3)parameters[2],
                        (T4)parameters[3],
                        (T5)parameters[4]);
            }
            return PatternMatch.this;
        }
    }

    public TR result() {
        return result;
    }

    public boolean haveMatch(Object... candidates) {
        if (matched) {
            // A match has already been found, so don't match again.
            return false;
        }

        // Are any unmatched?
        boolean areAnyDifferent =
                sequence(candidates) // These possible matches...
                        .zip(sequence(parameters)) // pair up with these inputs...
                        .map(areEqual) // make 'true' if they're equal...
                        .contains(false); // return true on the first non-match.

        // Have all parameters matched?
        return matched = !areAnyDifferent;
    }

    // Wildcard object matches anything.
    public static final class Wildcard {
        private Wildcard() {
        }

        @SuppressWarnings("EqualsWhichDoesntCheckParameterClass")
        @Override
        public boolean equals(Object o) {
            return true;
        }

        @Override
        public String toString() {
            return "Wildcard";
        }
    }

    public static final Callable1<Pair<Object, Object>, Boolean> areEqual = new Callable1<Pair<Object, Object>, Boolean>() {
        public Boolean call(Pair<Object, Object> pair) {
            Object p1 = pair.first();
            Object p2 = pair.second();
            if (p1 == null) {
                return p2 == null;
            }
            return p1.equals(p2);
        }
    };
}

This code is completely free to use by anybody. It is held under the Do What You Want To Public License: http://tinyurl.com/DWYWTPL

No comments:

Post a Comment