Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Structural Pattern Matching #1087

Closed
danieldietrich opened this issue Feb 6, 2016 · 29 comments
Closed

Structural Pattern Matching #1087

danieldietrich opened this issue Feb 6, 2016 · 29 comments

Comments

@danieldietrich
Copy link
Member


Note: I know that this will be a big change, directly before the release of 2.0.0. But Scala-like structural pattern matching for Java will be a real game changer if done right! We need to do this - now. Changing the Match API directly after 2.0.0 is no viable solution.


List<Tuple3<String, Integer, Double> list = ...;

//
// x  is of type Tuple3<String, Integer, Double>
// xs is of type List<Tuple3<String, Integer, Double>>
//
list.match()
    .when(List($(), $()))
    .then((x, xs) -> ...);

// Match.of(list) also works

//
// s is of type String
//
list.match()
    .when(List(Tuple3($("begin"), _, _), _))
    .then(s -> ...);

The basic syntax

  • Match.when(pattern).then(function)
  • There will be atomic patterns,
    • named any-value: $(), e.g. when(List($(), $())).then((x, xs) -> ...)
    • named specifc-value: $("test"), e.g. when(List($(1), List.nil())).then(one -> ...)
    • ignoring any-matcher: _, e.g. when(List(_, $())).then(tail -> ...)
  • There will be patterns for types, e.g. List(...), all upper-case
  • There will be matchers like is(...), isIn(...), all lower-case

Changes to the current Match API:

  • The Match implementation will get simpler. The core will fit on one page + generated duplication for arity 0..8 of pattern return values, all methods literally one-liners.

  • MatchFunction will pass away. Maybe we can replace it by o -> Match.of(o).when(...).then(...) or by Match.of(o).when(...).then(...).toFunction() if a function is needed.

  • Never liked the fact that Match grew to an internal DSL, like whenIs, whenIsIn, whenType, whenThis, whenThat. We see similar things in a wide range of other libs, like when().is(), when().isIn(), ... Instead we will compose the match expression with other functions like this:

    list.match()
        .when(is(val))
        .then(v -> ...);
    
    list.match()
        .when(isIn(val1, val2, val3))
        .then(v -> ...);

Match will gain importance

  • it will move to the first level package javaslang

  • it will be as easy to use as 1-2-3.

    import static javaslang.Match.*; // standard patterns $_, $(), $(val), is(), isIn(), type(), ...
    import static javaslang.Patterns.*; // Javaslang types List(x, xs), Tuple3(_1, _2, _3), ...
  • it will be extensible, we will be able to create Patterns for existing final classes (without source)

    // we will ship patterns for JDKs. Maybe this needs to be a module for every jdk version
    import javaslang.JDK8Patterns;
    
    // we provide a compile-time code generator for new custom patterns.
    @Patterns
    interface My {
    
        @Pattern(List.class)
        interface List {
            // to be done
        }
    
        @Pattern(Tuple3.class)
        interface Tuple3 {
            // to be done
        }
    
    }
    
    // code generation result
    public final class MyPatterns {
    
        // -- List patterns
        public static Pattern0 List() 
        public static Pattern1 List(Pattern0, Pattern1)
        public static Pattern1 List(Pattern1, Pattern0)
        public static Pattern2 List(Pattern0, Pattern2)
        public static Pattern2 List(Pattern1, Pattern1)
        public static Pattern2 List(Pattern2, Pattern0)
        // ...
    
        // -- Tuple3 patterns
        // ...
    }
@danieldietrich danieldietrich self-assigned this Feb 6, 2016
@danieldietrich danieldietrich added this to the 2.0.0 milestone Feb 6, 2016
@JohnMcGuinness
Copy link

_ will be an illegal identifier in JDK9. http://openjdk.java.net/jeps/213

@danieldietrich
Copy link
Member Author

Yes, I know. Used it in the prototype in order to make it look more like Scala but it will probably change the name. Or we convince the Java guys to leave it :)

@danieldietrich
Copy link
Member Author

We need a placeholder for any value.

Example:

// Scala
match o {
    case Some(_) => ...
    case None => ...
}
// Javaslang prototype
match(o)
    .when(Some(_)).then(...)    // <---- _ not supported any more with JDK 9 :-/
    .when(None).then(...)

Which alternatives do we have?

E.g. __ is valid...

// Javaslang prototype
match(o)
    .when(Some(__)).then(...)
    .when(None).then(...)

match(t)
    .when(Tuple1(__)).then(...)
    .when(Tuple2(__, __)).then(...)
    .when(Tuple3(__, __, __)).then(...)
    .when(__).then(...)

...but lame.

@danieldietrich
Copy link
Member Author

I was tempted to switch to Javaslang's original caze syntax ;) E.g. instead of otherwise(...) we will have caze(_).

match(option)
    .caze(Some(_)).then("It is a Some!")
    .caze(_)      .then("It is a None!")

Maybe also no then:

match(option)
    .caze(Some(_), "It is a Some!")
    .caze(_,       "It is a None!")

Another example:

// Scala: ????? don't know how to do that
val list = List(Some(1), Some(2), Some(3))
list match {
    case x :: xs => ???
}
// Javaslang
List<Option<Integer>> list = List.of(Option.some(1), Option.some(2), Option.some(3));
match(list)
    .caze(List(Some($(1)), List($(), _)), (one, intOption) -> ...)

@evacchi
Copy link

evacchi commented Feb 6, 2016

How about $_ ?

@danieldietrich
Copy link
Member Author

Yes, that would be a good fit to the other matchers $() and $(val)!

@evacchi
Copy link

evacchi commented Feb 6, 2016

Yup that was my thinking!

@danieldietrich
Copy link
Member Author

👍 I will use $_ in the prototype!

@evacchi
Copy link

evacchi commented Feb 6, 2016

Cool :)

@danieldietrich
Copy link
Member Author

Our Match API has methods

public <T1> Then1<T, T1> when(Pattern1<T, T1> pattern) { ... }
public <T1, T2> Then2<T, T1, T2> when(Pattern2<T, T1, T2> pattern) { ... }
public <T1, T2, T3> Then3<T, T1, T2, T3> when(Pattern3<T, T1, T2, T3> pattern) { ... }
// ... up to when(Pattern8)

In a previous prototype Pattern0 to Pattern8 where interfaces.

@FunctionalInterface
interface Pattern0 {
    Option<Void> apply(Object o);
}

@FunctionalInterface
interface Pattern1<T, T1> {
    Option<T1> apply(Object o);
}

@FunctionalInterface
interface Pattern2<T, T1, T2> {
    Option<Tuple2<T1, T2>> apply(Object o);
}

@FunctionalInterface
interface Pattern3<T, T1, T2, T3> {
    Option<Tuple3<T1, T2, T3>> apply(Object o);
}

// ... up to Pattern8

The compiler warns that these interfaces raise ambiguities when used as args in when, see above. Even if the @FunctionalInterface annotation is removed, the use of similar SAM interfaces will still raise possible ambiguities.

I'm curious if it really fails at runtime but instead of going deeper I will use the existing prototype and will focus on a complete solution.

Currently the solution is to use abstract classes instead. Then the when method signatures are uniform by argument type and the compiler does not complain any more.

abstract class Pattern0 {
    public abstract Option<Void> apply(Object o);
}

abstract class Pattern1<T, T1> {
    public abstract Option<T1> apply(Object o);
}

abstract class Pattern2<T, T1, T2> {
    public abstract Option<Tuple2<T1, T2>> apply(Object o);
}

abstract class Pattern3<T, T1, T2, T3> {
    public abstract Option<Tuple3<T1, T2, T3>> apply(Object o);
}

// ... up to Pattern8

However, it would be great to use lambdas instead of abstract classes. For benchmarks lambda vs. abstract class see http://www.oracle.com/technetwork/java/jvmls2013kuksen-2014088.pdf

Maybe someone has the time to investigate if Pattern0 to Pattern8 can be interfaces or if it is possible in general to use interfaces instead of abstract classes. See javaslang.Match and javaslang.MatchTest.

@danieldietrich
Copy link
Member Author

I think that it will make sense to add guards, e.g.

// like Scala's match o { case ... if ... => ... }
List<Integer> list = ...;
Match.of(list)
     .when(List($(), $()))
     .with((x, ignored) -> x >= 0)
     .then((x, xs) -> ...);

Naming: Could be also

Match.of(...).when(...).with(...).then(...);
Match.of(...).when(...).and(...).then(...);
Match.of(...).when(...).given(...).then(...);
Match.of(...).when(...).guardedBy(...).then(...);
Match.of(...).when(...).provided(...).then(...);
// etc.

@danieldietrich
Copy link
Member Author

To align thy syntax to Scala I propose the following:

match(o)
    ._case(...)._if(...).then(...)
    ._case(...).then(...);

@evacchi
Copy link

evacchi commented Feb 6, 2016

Personally I'm not a fan of using _ in identifiers. You may be able to avoid it if it were acceptable to capitalize some identifiers. e.g.:

Match(o).of(
  Case(...).then(...), // notice that Case here would be a class 
  Case(...).then(...)
)

just throwing it in there.

@danieldietrich
Copy link
Member Author

Yes, that looks much cleaner but also a little like C# :-) If will be a method but needs to be upper-case here.
The nice thing about the of-scope is that it might enable us to make exhaustive checks. Need to verify that. Another nice thing about the of-scope is that this will simplify the implementation I think - great idea!

Also it would be a clean API to have constructors like Scala's companion apply methods, e.g. List(...) instead of List.of(...). That would align to Match(o) and Case(...).

Also the naming schema for patterns is not 100% clear. We could use List() or Nil, List(x, xs) or Cons(x, xs) (in Scala: x :: xs). But I think we need to take a different approach than Scala here and have List() and List(x, xs).

Will come back tomorrow to these ideas. gdn8!

@danieldietrich
Copy link
Member Author

@evacchi Looks more than good to me - you are a genius! This can be used to automatically return the upper type bound of all then()-results :-)))

Here is a little test, which returns BigDecimal in one case and Integer in the other case. The overall result is correctly recognized as Number! I thought this is not possible, currently we give a type-hint in such cases in Javaslang (Match.as(Number.class)...).

Now, I found a new hobby. Add unused generics to methods that provide the compiler with types that help it during compilation. I think that might be called 'phantom type' (see Phantom Types in Scala).

I've used similar type-magic for structural pattern matching to transport types back-and-forth through the recursive structure of objects. Later I read that article mentioned above. Didn't know that Java's type system is capable of such things and still don't fully understand it.

// only one import for all types and methods!
import static javaslang.Match.*;

class Test {{

    Object o = new Object();
    Pattern p = new Pattern() {};

    // Number (!) <-- upper type bound of all results computed here :)
    Match(o).of(
            Case(p).then(() -> new BigDecimal("1")),
            Case(p).then(() -> 1)
    );

}}

Based on this:

import java.util.function.Supplier;

public final class Match<T> {

    static <T> Match<T> Match(T t) {
        return new Match<>();
    }

    // upper type bound is auto-magically computed with SUP, although unused
    @SafeVarargs
    final <SUP extends R, R> R of(Then<R>... cases) {
        return null;
    }

    // I'm sure T needs to be 'transported' from Match to Pattern to Case
    static Case Case(Pattern p) {
        return new Case();
    }

    static final class Case {
        <R> Then<R> then(Supplier<? extends R> o) {
            return new Then<>();
        }
    }

    static final class Then<R> {}

    static abstract class Pattern {}
}

@danieldietrich
Copy link
Member Author

I like the API

Match(o).of(
  Case(pattern).then(...)
  Case(...).then(...)
)

but it is not possible to inject the type of o into the Case(...) method call. Then the pattern does not 'know' the type of o at compile time and we can't decompose o with matchers $(), $(val).

It does not matter. Then we fall back to the original API. It is just about naming things.

The upper type bound computation would be cool though.

@danieldietrich
Copy link
Member Author

Awesome, I've got a working example here for both result bound computation and type-safe patterns.

This (modulo naming) will be the syntax. All other possibilities do not work (see below).

The following tests are all about types. They do not implement structural pattern matching.

WORKING

public class ScopedResultTest {

    public static void main(String[] args) {

        // upper bound of result computed correctly
        final Option<Number> num = Match(List.of(1)).of(
                // DOES CORRECTLY NOT COMPILE, BECAUSE "1" is not int:
                // Case(List("1"), o -> Option.of(1)),
                Case(List(1), list -> Option.of(new BigDecimal("1"))),
                Case(List(2), list -> Option.of(Double.NaN))
        );

    }

    static <T> MatchBuilder<T> Match(T t) {
        return new MatchBuilder<>();
    }

    static <T, U, R> Case<T, R> Case(Pattern1<T, U> p, Function<? super U, ? extends R> f) {
        return new Case<>();
    }

    static final class MatchBuilder<T> {
        @SafeVarargs
        final <SUP extends R, R> R of(Case<T, ? extends R>... cases) { return null; }
    }

    static final class Case<T, R> {}

    static abstract class Pattern1<T, T1> {
    }

    static <T extends List<U>, U> Pattern1<List<U>, T> List(U t) {
        return null;
    }
}

NOT WORKING

public class ScopedResultTest2 {

    public static void main(String[] args) {

        // upper bound computed
        final Option<Number> num = Match(List.of(1),
                // ERROR: List("1") DOES COMPILE BUT SHOULD NOT
                Case(List("1"), o -> Option.of(1)),
                Case(List(1), list -> Option.of(new BigDecimal("1"))),
                Case(List(2), list -> Option.of(Double.NaN))
        );

    }

    @SafeVarargs
    static <T, SUP extends R, R> R Match(T t, Case<T, ? extends R>... cases) { return null; }

    static <T, U, R> Case<T, R> Case(Pattern1<T, U> p, Function<? super U, ? extends R> f) {
        return new Case<>();
    }

    static final class Case<T, R> {}

    static abstract class Pattern1<T, T1> {
    }

    static <T extends List<U>, U> Pattern1<List<U>, T> List(U t) {
        return null;
    }
}
@SuppressWarnings("ConstantConditions")
public class FluentResultTest {

    public static void main(String[] args) {

        // TYPE HINT DOES NOT WORK FOR GENERICS LIKE Option<String>
        final Option option = Match(List.of(1), Option.class)
                // DOES CORRECTLY NOT COMPILE, BECAUSE "1" is not int:
                // .Case(List("1")).then(i -> 1)
                .Case(List(1)).then(list -> Option.of(new BigDecimal("1")))
                .Case(List(2)).then(list -> Option.of(Double.NaN))
                .get();

    }

    static <T, R> MatchBuilder<T, R> Match(T t, Class<R> hint) {
        return new MatchBuilder<>();
    }

    static final class MatchBuilder<T, R> {

        <T1> Case1<T, T1, R> Case(Pattern1<T, T1> p) {
            return null;
        }

        R get() {
            return null;
        }
    }

    interface Case1<T, T1, R> {
        MatchBuilder<T, R> then(Function<? super T1, ? extends R> f);
    }

    static abstract class Pattern1<T, T1> {
    }

    static <T extends List<U>, U> Pattern1<List<U>, T> List(U t) {
        return null;
    }
}

@danieldietrich
Copy link
Member Author

I added the new Match API to my fork - and removed the old one.

In order to prevent diverging from HEAD too much I want to create a PR neartime.

Match is fully working, but raw. Before we merge the changes into the head, we need to

  • add Patterns. This implies that we have a working annotation processor that generates the necessary Pattern code. For hand-crafted test-Patterns see javaslang.MatchTest

After that, the following needs to be done:

  • re-think Matchers. Do we really need them? I Think the new API can do better, if we add if-guards.

    • is(T) - or maybe Case(T, f)
    • isIn(T...)
    • type(Class<T>)
    • typeIn(Class<?>)
    • ...
  • add if-guards

    // guard : Predicate<...>, f : Function<..., R>
    Match(o).of(
            Case(pattern, guard, f),
    );
  • Re-writing Match Tests. The old ones are saved but commented out.

  • Value.match() currently returns Match<Value> but we need to change this method to Value.match(Cases...) to have syntactic sugar like this:

    // default way
    Match(list).of(
            Case(List($(), $()), (x, xs) -> "head: " + x + ", tail: " + xs),
            Case($_, -> "Nil")
    );
    
    // syntactic sugar
    list.match(
            Case(List($(), $()), (x, xs) -> "head: " + x + ", tail: " + xs),
            Case($_, -> "Nil")
    );

@danieldietrich
Copy link
Member Author

Status update

I made progress with the code generator. It should be usable now.

Additionally the picture gets more and more clear what to generate and how to model the things we want to generate.

Example:

Let's consider some arbitrary, user-defined class we want to pattern match. Please note, that it is declared final.

static final class Developer implements Person {
    private final String name;
    private final boolean isCaffeinated;

    Developer(String name, boolean isCaffeinated) {
        this.name = name;
        this.isCaffeinated = isCaffeinated;
    }

    public String getName() { return name; }

    public boolean isCaffeinated() { return isCaffeinated; }
}

Our goal is to have a static method Developer that can be used as pattern in a Case statement:

Developer dev = new Developer("Grobi", true);
Match(dev).of(
        Case(Developer($(), true), name -> name + " is caffeinated!"),
        Case($_, "catch all")
);

This is how the pattern-declaration should look like:

// USER-DEFINED
@Patterns
class My {
    @Unapply
    static Tuple2<String, Boolean> Developer(Developer dev) {
        return Tuple.of(dev.getName(), dev.isCaffeinated());
    }
}

Then, at compile time, a class MyPatterns in the same package is generated. This class will look like this:

// THIS IS THE GENERATED PART
public final class MyPatterns {

    private MyPatterns() {}

    public static <T1, T2> Pattern2<Developer, T1, T2> Developer(Pattern1<String, T1> p1, Pattern1<Boolean, T2> p2) {
        return Pattern2.create(Developer.class, My::Developer, p1, p2);
    }

    // + many more methods (in general) for all variations of Pattern0, ..., Pattern8, InversePattern and plain values of type T.
    // REMINDER TO MYSELF: some types are atomic and need not to be pattern matched deeper (String, Integer, Boolean, ...)
}

Because My is package-private (will be checked by the annotation processor), it will be no public API. Users will only have to import static my.package.MyPatterns.* to use the patterns.

The Pattern2 factory method will look like this (hidden, part of Javaslang):

public static abstract class Pattern2<T, T1, T2> {

    public abstract Option<Tuple2<T1, T2>> apply(Object o);

    public static <TYPE, A1, A2, T1, T2> Pattern2<TYPE, T1, T2> create(
            Class<TYPE> matchableType,
            Function<TYPE, Tuple2<A1, A2>> unapply,
            Pattern1<A1, T1> p1,
            Pattern1<A2, T2> p2) {
        return new Pattern2<TYPE, T1, T2>() {
            @Override
            public Option<Tuple2<T1, T2>> apply(Object o) {
                if (o != null && matchableType.isAssignableFrom(o.getClass())) {
                    @SuppressWarnings("unchecked")
                    final TYPE matchable = (TYPE) o;
                    final Tuple2<A1, A2> t = unapply.apply(matchable);
                    return p1.apply(t._1).flatMap(v1 -> p2.apply(t._2).map(v2 -> Tuple.of(v1, v2)));
                } else {
                    return Option.none();
                }
            }
        };
    }
}

Next

This looks pretty straight forward now. I think the trickiest part is to extract the generics of the method args during annotation processing. Will dive into that...

@danieldietrich
Copy link
Member Author

Other pattern declarations look like this:

@Patterns
class My {

    @Unapply static <T> Tuple1<T> Some(Option.Some<T> some) { return Tuple.of(some.get()); }
    @Unapply static Tuple0 None(Option.None<?> none) { return Tuple.empty(); }

    @Unapply static <T> Tuple2<T, List<T>> Cons(List.Cons<T> cons) { return Tuple.of(cons.head(), cons.tail()); }
    @Unapply static Tuple0 Nil(List.Nil<?> nil) { return Tuple.empty(); }

    // followed by many other patterns and maybe (private) helper methods

}

This should be all what is needed to make user-types pattern-matchable. All other things happen auto-magically at compile-time.

@danieldietrich
Copy link
Member Author

Yay! The generic type information is accessible!!!

Source which is interpreted by annotation processor:

@Patterns
class Moo {

    // Option
    @Unapply static <T> Tuple1<T> Some(Option.Some<T> some) { return Tuple.of(some.get()); }
    @Unapply static Tuple0 None(Option.None<?> none) { return Tuple.empty(); }

    // List
    @Unapply static <T> Tuple2<T, List<T>> Cons(List.Cons<T> cons) { return Tuple.of(cons.head(), cons.tail()); }
    @Unapply static Tuple0 Nil(List.Nil<?> nil) { return Tuple.empty(); }

    // Developer
    @Unapply static Tuple2<String, Boolean> Developer(Developer dev) { return Tuple.of(dev.getName(), dev.isCaffeinated()); }

    // TEST!
    void non_static_method() {}
    Tuple2<String, Boolean> no_annotation(Developer dev) { return Tuple.of(dev.getName(), dev.isCaffeinated()); }

}

Programmatically extracted:

METHOD: <T>Some(javaslang.control.Option.Some<T>)
  TYPE MIRROR: <T>(javaslang.control.Option.Some<T>)javaslang.Tuple1<T>
  RETURN TYPE: javaslang.Tuple1<T>
    KIND: DECLARED
    DECLARED TYPE: javaslang.Tuple1<T>
      TYPE ARGS: T
  TYPE PARAMETER:
    GENERIC: <T>Some(javaslang.control.Option.Some<T>)
    TYPE MIRROR: T
METHOD: None(javaslang.control.Option.None<?>)
  TYPE MIRROR: (javaslang.control.Option.None<?>)javaslang.Tuple0
  RETURN TYPE: javaslang.Tuple0
    KIND: DECLARED
    DECLARED TYPE: javaslang.Tuple0
      TYPE ARGS: 
METHOD: <T>Cons(javaslang.collection.List.Cons<T>)
  TYPE MIRROR: <T>(javaslang.collection.List.Cons<T>)javaslang.Tuple2<T,javaslang.collection.List<T>>
  RETURN TYPE: javaslang.Tuple2<T,javaslang.collection.List<T>>
    KIND: DECLARED
    DECLARED TYPE: javaslang.Tuple2<T,javaslang.collection.List<T>>
      TYPE ARGS: T,javaslang.collection.List<T>
  TYPE PARAMETER:
    GENERIC: <T>Cons(javaslang.collection.List.Cons<T>)
    TYPE MIRROR: T
METHOD: Nil(javaslang.collection.List.Nil<?>)
  TYPE MIRROR: (javaslang.collection.List.Nil<?>)javaslang.Tuple0
  RETURN TYPE: javaslang.Tuple0
    KIND: DECLARED
    DECLARED TYPE: javaslang.Tuple0
      TYPE ARGS: 
METHOD: Developer(javaslang.test.Developer)
  TYPE MIRROR: (javaslang.test.Developer)javaslang.Tuple2<java.lang.String,java.lang.Boolean>
  RETURN TYPE: javaslang.Tuple2<java.lang.String,java.lang.Boolean>
    KIND: DECLARED
    DECLARED TYPE: javaslang.Tuple2<java.lang.String,java.lang.Boolean>
      TYPE ARGS: java.lang.String,java.lang.Boolean

Now it is 'only' the processing logic which has to be implemented.

@danieldietrich
Copy link
Member Author

I've merged a first version.

Building Patterns at Compile Time

Every project that uses Javaslang can define its own patterns for pattern matching. In addition to Javaslang, it needs also this dependency:

<dependency>
    <!-- will be changed to io.javaslang soon -->
    <groupId>com.javaslang</groupId>
    <artifactId>javaslang-match</artifactId>
    <!-- don't depend on snapshots! -->
    <version>2.0.0-RC5-SNAPSHOT</version>
    <scope>compile</scope>
</dependency>

We define Patterns this way:

  • @Patterns class My will be class MyPatterns.
  • @Patterns class $ will be class Patterns. (special case / syntactic sugar)

I've pre-defined first patterns for test purpose:

@javaslang.match.Patterns
class $ {

    // Option
    @Unapply static <T> Tuple1<T> Some(Option.Some<T> some) { return Tuple.of(some.get()); }
    @Unapply static Tuple0 None(Option.None<?> none) { return Tuple.empty(); }

    // List
    @Unapply static <T> Tuple2<T, List<T>> Cons(List.Cons<T> cons) { return Tuple.of(cons.head(), cons.tail()); }
    @Unapply static Tuple0 Nil(List.Nil<?> nil) { return Tuple.empty(); }

}

When compiling the project, the annotation processor runs automatically and creates our patterns. I've checked our patterns into version control. This is how the generated code looks like (will be simplified a bit soon).

Using Pattern Matching

First we need to import the general Match API and specific Patterns.

import static javaslang.Match.*;
import static javaslang.Patterns.*;

Then we can start to match objects. The next days I will add Patterns for all Javaslang objects. Patterns for other Java objects might follow.

// our test object
Option<Tuple2<String, Integer>> TUPLE2_OPTION = Option.of(Tuple.of("Test", 123));

// a first test
Match(TUPLE2_OPTION).of(
        Case(Some($()), value -> {
            Tuple2<String, Integer> tuple2 = value; // types are inferred correctly!
            ...
        })
);

More examples:

List<String> list = List.empty();

Match(list).of(
        Case(Cons("1", $_), () -> "starts with 1"),
        Case(Nil, () -> "empty")
);

// current syntactic sugar, works for all Javaslang Values
list.match().of(
        Case(Cons("1", $_), () -> "starts with 1"),
        Case(Nil, () -> "empty")
);

// will be soon changed to, plus additional sugar
list.match(
        Case(Cons("1", $_), "starts with 1"),
        Case(Nil, "empty")
);

But it is still an early draft.

We currently have ambiguities because I allowed to match generic values:

Cons(1, List.of(1, 2, 3))

This leads to

// `Cons($(), $())` results in `(x, xs) -> ...`
static <__, T> void Cons(InversePattern<? extends T> p1, InversePattern<? extends List<T>> p2) {
}

// Ambiguous to the above, `Cons($(), $())` does not compile any more
static <__, T> void Cons(T p1, InversePattern<? extends List<T>> p2) {
}

But that's no problem. Beside removing generic values at all, we have several options to fix it. Here are some simple examples (which scale):

Solution 1: Add additional generic parameters with extends relation

static <__, T, T2 extends T> void Cons(T p1, InversePattern<? extends List<T2>> p2) {
}

Solution 2: Cons(1, $()) and Cons("1", $()) do both compile. This is unsafe!

static <__, T> void Cons(Object p1, InversePattern<? extends List<T>> p2) {
}

Solution 3: Cons(Eq(1), $()) Syntax unsatisfying/too complicated.

static <__, T> void Cons(EqualsPattern<? extends T> p1, InversePattern<? extends List<T>> p2) {
}

Currently I prefer the suggested Solution 1 but have to investigate it a bit.

Next steps:

  • Complete patterns for unapplying all Javaslang objects
  • Tests, tests, tests
  • Take a look at the TODOs in Match / Generator.scala
  • Change documentation

@danieldietrich
Copy link
Member Author

I want to keep it even simpler, just import

import static javaslang.Predef.*;

And then use

  • basic Match API
  • Match Patterns for all Javaslang types
  • and maybe soon also For comprehension API

@danieldietrich
Copy link
Member Author

In order to solve the recursive pattern matching problem, we generate methods for all possible combinations of pattern arities - theoretically. Practically, we have an upper bound - currently the maximum tuple arity (because the unapply result is a tuple).

Example: Let's consider a Tuple2<Option<String>, Option<Integer>>.

If we use a pattern Tuple2($_, $_) the method Tuple2(Pattern0, Pattern0) is called, whereas a the use of Tuple2($(), $_) leads to a call of Tuple2(InversePattern, Pattern0).

Having possible parameter types { T, InversePattern, Pattern0, Pattern1, ..., Pattern8 } this leads to a significant amount of method signatures with increasing parameter count.

We have 11 pattern types (see. above). The number of methods is therefore 11^number_of_args, namely

args methods
0 1
1 11
2 121
3 1331
4 14641
5 161051
6 1771561
7 19487171
8 214358881

It is clear that we can't generate lookup-tables of that much methods. Instead we will do it the pragmatic way. Objects may be unapplied to 0, 1 or 2 elements. I.e. we will generate these variations:

args methods
0 1
1 11
2 121

If objects are deconstructed to so-called atomic types that cannot be further deconstructed (by definition), e.g. Integer, Byte, String, ..., then the generated method count reduces to 4^number_of_args (4 possible patterns: { T, InversePattern, Pattern0, Pattern1 }).

args methods
3 64
4 256
5 1024
6 4096
7 16384
8 65536

That is still too much for arguments > 3 or 4.

We will be still able to filter deconstructed values with guards, e.g.

Match(tuple8).of(
    Case(t -> equals(t._1, t._2), // guard
         t -> ...) // result
)

A compiler could reduce all this to 1 method per unapply per case. But I think this is currently not possible with Java because we can't hook into the compiler like Scala with Scala Macros.

However, real pattern matching for up to 2 args is still great because it should fit for nearly all Javaslang Value types (but not Tuples).

@danieldietrich
Copy link
Member Author

I think I've found a way to reduce the number of generated method per unapply method to ... just 1.

However, the behavior will change.

  1. We can still recognize patterns of arbitrary depth
  2. But only the properties of the given/matched object are extracted/unapplied

Also we will have just two atomic matchers and we have to pass patterns instead of arbitrary objects:

  • $() will match any object
  • $(object) will match object
Option<Option<Option<String>>> option = Option.of(Option.of(Option.of("ok")));

// here all x denote the value of the given option, if it is a Some
final Number number = Match(option).of(
       Case(None(), () -> new BigDecimal("1")),
       Case(Some(None()), x -> (byte) 2),
       Case(Some(Some(Some($("ok")))), x -> 1.0d),
       Case(Some(Some(Some($()))), x -> 3.0d)
);

Especially we cannot write Case(Some("string"), () -> ...) any more. Instead we write Case(Some($("string")), s -> ...).

Note: Beside the number of generated methods there is one significant benefit: The user does not have to check the order of deeply extracted parameters within the pattern tree. We now just unapply the values of the given object to be matched regardless a match of inner objects.

@danieldietrich
Copy link
Member Author

Idea: Having generated Match Patterns at hand, we could use them to define an object query language to extract specific parts from an object graph. This is roughly the same as pattern matching. But as we already saw, practically we can't pre-calculate the number of extracted objects by just using the type system. That would involve too many pre-generated patterns.

But maybe it is possible to traverse the object pattern graph and collect the results in an HList (see #237) instead of a Tuple.

This will not be implemented for 2.0.0. I just wanted to write down the idea here. The current API isn't capable of that. Perhaps that would raise the need to introduce a separate internal object query dsl based on the basic pattern matching ideas described here.

@danieldietrich
Copy link
Member Author

Idea: Add an additional atomic pattern $(Predicate) that matches only, if the predicate is fulfilled.

List<Integer> ints = ...;

Match(ints).of(
    Case(Cons($(i -> i > 2), $()), (x, xs) -> ...)
);

// also possible
Match(ints).of(
    Case(Cons($(1), Cons($(i -> i > 2), $())), (x, xs) -> ...)
);

Does <T> ... $(Predicate<? super T>) clash with <T> ... $(T)?

Update: Seems not to clash :)

@danieldietrich
Copy link
Member Author

This should also be possible:

Match(str).of(
    Case($("one"), 1),
    Case($("two"), 2),
    Case($(), 3)
)

which is roughly equivalent to Scala's

str match {
    case "one" => 1
    case "two" => 2
    case _ => 3
}

I.e. in addition to the methods

Case(Pattern, Function)

we need to return just values without using a lambda to calculate the result

Case(Pattern, T)

@danieldietrich
Copy link
Member Author

The final core Match API has been committed with #1167 and #1168. Further changes (adding standard patterns, predicates etc.) will be tracked in #1157.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants