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
Comments
_ will be an illegal identifier in JDK9. http://openjdk.java.net/jeps/213 |
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 :) |
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. // 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. |
I was tempted to switch to Javaslang's original match(option)
.caze(Some(_)).then("It is a Some!")
.caze(_) .then("It is a None!") Maybe also no 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) -> ...) |
How about |
Yes, that would be a good fit to the other matchers |
Yup that was my thinking! |
👍 I will use $_ in the prototype! |
Cool :) |
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 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 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 |
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. |
To align thy syntax to Scala I propose the following: match(o)
._case(...)._if(...).then(...)
._case(...).then(...); |
Personally I'm not a fan of using Match(o).of(
Case(...).then(...), // notice that Case here would be a class
Case(...).then(...)
) just throwing it in there. |
Yes, that looks much cleaner but also a little like C# :-) 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! |
@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 {}
} |
I like the API Match(o).of(
Case(pattern).then(...)
Case(...).then(...)
) but it is not possible to inject the type of 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. |
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;
}
} |
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
After that, the following needs to be done:
|
Status updateI 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 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 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 // 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 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();
}
}
};
}
} NextThis 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... |
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. |
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:
Now it is 'only' the processing logic which has to be implemented. |
I've merged a first version. Building Patterns at Compile TimeEvery 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:
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 MatchingFirst 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: static <__, T> void Cons(Object p1, InversePattern<? extends List<T>> p2) {
} Solution 3: 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:
|
I want to keep it even simpler, just import import static javaslang.Predef.*; And then use
|
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 If we use a pattern Having possible parameter types We have 11 pattern types (see. above). The number of methods is therefore 11^number_of_args, namely
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:
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 }).
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). |
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.
Also we will have just two atomic matchers and we have to pass patterns instead of arbitrary objects:
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 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. |
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 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. |
Idea: Add an additional atomic pattern 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 Update: Seems not to clash :) |
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) |
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.
The basic syntax
Match.when(pattern).then(function)
$()
, e.g.when(List($(), $())).then((x, xs) -> ...)
$("test")
, e.g.when(List($(1), List.nil())).then(one -> ...)
_
, e.g.when(List(_, $())).then(tail -> ...)
List(...)
, all upper-caseis(...)
,isIn(...)
, all lower-caseChanges 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 byMatch.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:
Match will gain importance
it will move to the first level package
javaslang
it will be as easy to use as 1-2-3.
it will be extensible, we will be able to create Patterns for existing final classes (without source)
The text was updated successfully, but these errors were encountered: