Java does not (yet) have pattern matching although writing a legible DSL for it is not too hard. I'm certainly not the first to take a hand at it.
An example DSL in action. Lambdas and method references sure help a lot:
public static void main(final String... args) { asList(0, 1, 2, 3, 13, 14, null, -1).stream(). peek(n -> out.print(format("%d -> ", n))). map(matching(Integer.class, Object.class). when(Objects::isNull).then(n -> "!"). when(is(0)).then(nil()). when(is(1)).then("one"). when(is(13)).then(() -> "unlucky"). when(is(14)).then(printIt()). when(even()).then(scaleBy(3)). when(gt(2)).then(dec()). none().then("no match")). map(MatchingTest::toString). forEach(out::println); out.flush(); // Avoid mixing sout and serr matching(Integer.class, Void.class). none().thenThrow(RuntimeException::new). apply(0); }
Implementation:
/** * {@code Matching} represents <a href="https://en.wikipedia.org/wiki/Pattern_matching">Pattern * Matching</a> in Java as a function production an optional. Example: <pre> * asList(0, 1, 2, 3, 13, 14, null, -1).stream(). * peek(n -> out.print(format("%d -> ", n))). * map(matching(Integer.class, Object.class). * when(Objects::isNull).then(n -> "!"). * when(is(0)).then(nil()). * when(is(1)).then("one"). * when(is(13)).then(() -> "unlucky"). * when(is(14)).then(printIt()). * when(even()).then(scaleBy(3)). * when(gt(2)).then(dec()). * none().then("no match")). * map(MatchingTest::toString). * forEach(out::println);</pre> * <p> * <i>NB</i> — There is no way to distinguish from an empty optional if * there was no match, or if a match mapped the input to {@code null}, without * use of a {@link When#then(Object) sentinel value} or {@link * When#thenThrow(Supplier) thrown exception}. * <p> * <strong>NB</strong> — There is no formal destructuring, but this can * be simulated in the {@code Predicate} to {@link #when(Predicate) when}. * * @param <T> the input type to match against * @param <U> the output type of a matched pattern * * @author <a href="mailto:binkley@alumni.rice.edu">B. K. Oxley (binkley)</a> */ @NoArgsConstructor(access = PRIVATE) public final class Matching<T, U> implements Function<T, Optional<U>> { private final Collection<Case> cases = new ArrayList<>(); /** * Begins pattern matching with a new pattern matcher. * * @param inType the input type token, never {@code null} * @param outType the output type token, never {@code null} * @param <T> the input type to match against * @param <U> the output type of a matched pattern * * @return the pattern matcher, never {@code null} * * @todo Avoid the type tokens */ public static <T, U> Matching<T, U> matching(final Class<T> inType, final Class<U> outType) { return new Matching<>(); } /** * Begins a when/then pair. * * @param when the pattern matching test, never {@code null} * * @return the pattern continuance, never {@code null} */ public When when(final Predicate<? super T> when) { return new When(when); } /** * Begins a default when/then pair, always placed <strong>last</strong> in * the list of cases (evaluates no cases after this one). * * @return then pattern continuance, never {@code null} */ public When none() { return when(o -> true); } /** * Evaluates the pattern matching. * * @param in the input to match against, possibly {@code null} * * @return the match result (empty if no match), never {@code null} */ @Override public Optional<U> apply(final T in) { return cases.stream(). filter(c -> c.p.test(in)). findFirst(). map(c -> c.q.apply(in)); } @RequiredArgsConstructor(access = PRIVATE) public final class When { /** * Number of frames to discard when creating an exception for a match. * Very sensitive to implementation. This aids in understanding stack * traces from matching, discarding internal machinery and leaving the * actual throwing call at the top of the stack. */ private static final int N = 7; private final Predicate<? super T> when; /** * Ends a when/then pair, evaluating <var>then</var> against the input * if matched. * * @param then the pattern matching function, never {@code null} * * @return the pattern matcher, never {@code null} */ public Matching<T, U> then( final Function<? super T, ? extends U> then) { cases.add(new Case(when, then)); return Matching.this; } /** * Ends a when/then pair, returning <var>then</var> if matched. * * @param then the pattern matching value, possibly {@code null} * * @return the pattern matcher, never {@code null} */ public Matching<T, U> then(final U then) { cases.add(new Case(when, x -> then)); return Matching.this; } /** * Ends a when/then pair, evaluating <var>then</var> independent of * supplier if matched. * * @param then the pattern matching supplier, never {@code null} * * @return the pattern matcher, never {@code null} */ public Matching<T, U> then(final Supplier<? extends U> then) { cases.add(new Case(when, x -> then.get())); return Matching.this; } /** * Ends a when/then pair, evaluating <var>then</var> to {@code null} * if matched. * * @param then the input consumer, never {@code null} * * @return the pattern matcher, never {@code null} */ public Matching<T, U> then(final Consumer<? super T> then) { cases.add(new Case(when, o -> { then.accept(o); return null; })); return Matching.this; } /** * Ends a when/then pair, evaluating <var>then</var> independent of * supplier and throwing the new exception if matched. * * @param then the pattern matching exception supplier, never {@code * null} * * @return the pattern matcher, never {@code null} */ public Matching<T, U> thenThrow( final Supplier<RuntimeException> then) { cases.add(new Case(when, x -> { final RuntimeException e = then.get(); final List<StackTraceElement> stack = asList( e.getStackTrace()); e.setStackTrace(stack.subList(N, stack.size()). toArray(new StackTraceElement[stack.size() - N])); throw e; })); return Matching.this; } } @RequiredArgsConstructor(access = PRIVATE) private final class Case { private final Predicate<? super T> p; private final Function<? super T, ? extends U> q; } }
UPDATE: I recently found the very nice Derive4J annotation processor library which provides similar functionality in the form of Algebraic Data Types (ADTs), aka, Sum Types in functional languages.
No comments:
Post a Comment