Saturday, March 5, 2011

Using Exceptions for Pattern Matching in Java

For a while I've had an idea that Java's try/catch scaffolding could be used to simulate variable return types and execute different code based on that. I decided the simplest way to demonstrate this would be to implement a pattern matcher, so I've done that here. I don't think this is a particularly elegant solution to pattern matching in Java, and I don't advocate it's use - think of it more as a different way to think about exceptions in your code. If you do decide to use something like this in production code (for whatever reason) I recommend having the MultiReturnTypeFunction and Matcher extend the Function type in Google's Guava library.

The important classes are Case, Matcher, MultiReturnTypeFunction, and Switch.

Case is a special type of Exception which does not fill in its stack trace when thrown (for performance reasons). MultiReturnTypeFunction throws a variety of Cases, which are caught by Switch and matched against the available Matchers until a match is found. The Match's apply method is called and the result is the Switch's result.


public abstract class Case extends Exception {

    /**
     * It's important to override this to reduce the time it takes to throw.
     */
    @Override
    public Case fillInStackTrace() {
        return this;
    }
}


public interface Matcher<U> {

    public boolean matches(Case c);

    public U apply(Case c);
}



public interface MultiReturnTypeFunction<T> {

    /**
     * Never returns normally, always throws a Case.
     */
    public void apply(T param) throws Case;
}



public class Switch <T, U> {
   public <T, U> U match (MultiReturnTypeFunction gen, T param, Matcher<U> defaultCase, Matcher<U>... matchers) {
        try {
            gen.apply(param); // always throws a Case
        }
        catch (Case e) {
            for (Matcher<U> m : matchers)
                if (m.matches(e))
                    return m.apply(e);
            return defaultCase.apply(e);
        }
        throw new IllegalStateException("No matcher found for " + param + " in " + gen);
    }
}


I have some example code to show this in use. It's simply finds the sign of a series of random Integers and prints them to standard output.

First we need Case corresponding to each case - Positive, Negative, and Zero. We could also have null, but I think that's overkill for this example.


class CaseNegative extends Case {}
class CasePositive extends Case {}
class CaseZero extends Case {}


And the example code:


public class Main {
    public static void main(String[] args) {

        MultiReturnTypeFunction<Integer> signOf = new MultiReturnTypeFunction<Integer>() {
            public void apply(Integer param) throws Case {
                if (param > 0) {
                    throw new CasePositive();
                }
                if (param == 0) {
                    throw new CaseZero();
                }
                throw new CaseNegative();
            }
        };

        Matcher<String> throwExceptionMatcher = new Matcher<String>() {
            public boolean matches(Case c) {
                return true;
            }

            public String apply(Case c) {
                throw new IllegalStateException("No matching matcher found for " + c);
            }
        };

        Matcher<String> positiveValueMatcher = new Matcher<String>() {
            public boolean matches(Case c) {
                return c instanceof CasePositive;
            }

            public String apply(Case c) {
                return "Positive";
            }
        };

        Matcher<String> negativeValueMatcher = new Matcher<String>() {
            public boolean matches(Case c) {
                return c instanceof CaseNegative;
            }

            public String apply(Case c) {
                return "Negative";
            }
        };

        Matcher<String> zeroValueMatcher = new Matcher<String>() {
            public boolean matches(Case c) {
                return c instanceof CaseZero;
            }

            public String apply(Case c) {
                return "Zero";
            }
        };

        Switch<Integer, String> _switch = new Switch <Integer, String>();

        for (int i = 0; i < 10; i++)
        {
            int val = new Random().nextInt(3) - 1;
            String sign = _switch.match(signOf, val, throwExceptionMatcher,
                    zeroValueMatcher, negativeValueMatcher, positiveValueMatcher);

            System.out.println(val + " is " + sign);
        }
    }
}


1 comment:

  1. Sounds like a cry for help: "Please make it stop this java thing, please! Kill me!"

    ReplyDelete

As per Ward's Wiki (via Carlfish), I would appreciate it if you would, as a courtesy, use your real name when posting comments.