📜 ⬆️ ⬇️

Implementing Pattern Matching in Java

Many modern languages ​​support pattern matching at the language level. Java currently does not support pattern matching, but there are hopes that things can change in the future.


Pattern matching reveals to the developer the ability to write code more flexibly and more beautifully, while leaving it understandable.


Using the capabilities of Java 8, you can implement some features of pattern matching as a library. In this case, you can use both the statement and the expression.


Constant pattern allows you to check for equality with constants. In Java, a switch allows you to check for equal numbers, enumerations, and strings. But sometimes you want to check for equality of the constant of objects using the equals () method.


switch (data) { case new Person("man") -> System.out.println("man"); case new Person("woman") -> System.out.println("woman"); case new Person("child") -> System.out.println("child"); case null -> System.out.println("Null value "); default -> System.out.println("Default value: " + data); }; 

Currently the library supports the ability to check the values ​​of a variable with 6 constants.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.ConstantPattern.matches; matches(data, new Person("man"), () -> System.out.println("man"); new Person("woman"), () -> System.out.println("woman"); new Person("child"), () -> System.out.println("child"); Null.class, () -> System.out.println("Null value "), Else.class, () -> System.out.println("Default value: " + data) ); ```<cut/> <i><b>Tuple pattern</b></i>         . ```java switch (side, width) { case "top", 25 -> System.out.println("top"); case "bottom", 30 -> System.out.println("bottom"); case "left", 15 -> System.out.println("left"); case "right", 15 -> System.out.println("right"); case null -> System.out.println("Null value "); default -> System.out.println("Default value "); }; 

Currently the library supports the ability to specify 4 variables and 6 branches.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.TuplePattern.matches; matches(side, width, "top", 25, () -> System.out.println("top"); "bottom", 30, () -> System.out.println("bottom"); "left", 15, () -> System.out.println("left"); "right", 15, () -> System.out.println("right"); Null.class, () -> System.out.println("Null value"), Else.class, () -> System.out.println("Default value") ); 

Type test pattern allows you to simultaneously match the type and extract the value of a variable. In Java, for this we need to first check the type, cast to the type, and then assign the new variable.


 switch (data) { case Integer i -> System.out.println(i * i); case Byte b -> System.out.println(b * b); case Long l -> System.out.println(l * l); case String s -> System.out.println(s * s); case null -> System.out.println("Null value "); default -> System.out.println("Default value: " + data); }; 

Currently the library supports the ability to check the values ​​of a variable with 6 types.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.VerifyPattern.matches; matches(data, Integer.class, i -> { System.out.println(i * i); }, Byte.class, b -> { System.out.println(b * b); }, Long.class, l -> { System.out.println(l * l); }, String.class, s -> { System.out.println(s * s); }, Null.class, () -> { System.out.println("Null value "); }, Else.class, () -> { System.out.println("Default value: " + data); } ); 

Guard pattern allows you to simultaneously match the type and check for conditions.


 switch (data) { case Integer i && i != 0 -> System.out.println(i * i); case Byte b && b > -1 -> System.out.println(b * b); case Long l && l < 5 -> System.out.println(l * l); case String s && !s.empty() -> System.out.println(s * s); case null -> System.out.println("Null value "); default -> System.out.println("Default: " + data); }; 

Currently the library supports the ability to check the values ​​of a variable with 6 types and conditions.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.GuardPattern.matches; matches(data, Integer.class, i -> i != 0, i -> { System.out.println(i * i); }, Byte.class, b -> b > -1, b -> { System.out.println(b * b); }, Long.class, l -> l == 5, l -> { System.out.println(l * l); }, Null.class, () -> { System.out.println("Null value "); }, Else.class, () -> { System.out.println("Default value: " + data); } ); 

To simplify writing the condition, the developer can use the following functions for comparison: lessThan / lt, greaterThan / gt, lessThanOrEqual / le, greaterThanOrEqual / ge,
equal / eq, not Equal / ne. And in order to omit the conditions you can change: always / yes, never / no.


 matches(data, Integer.class, ne(0), i -> { System.out.println(i * i); }, Byte.class, gt(-1), b -> { System.out.println(b * b); }, Long.class, eq(5), l -> { System.out.println(l * l); }, Null.class, () -> { System.out.println("Null value "); }, Else.class, () -> { System.out.println("Default value: " + data); } ); 

Deconstruction pattern allows you to simultaneously match the type and decompose an object into its components. In Java, to do this, we first need to check the type, cast to the type, assign it to a new variable, and only then get the fields of the class through getters.


 let (int w, int h) = figure; switch (figure) { case Rectangle(int w, int h) -> out.println("square: " + (w * h)); case Circle(int r) -> out.println("square: " + (2 * Math.PI * r)); default -> out.println("Default square: " + 0); }; for ((int w, int h) : listFigures) { System.out.println("square: " + (w * h)); } 

At the moment, the library supports the ability to check the values ​​of a variable with 3 types and decompose an object into 3 components.


 import org.kl.state.Else; import static org.kl.pattern.DeconstructPattern.matches; import static org.kl.pattern.DeconstructPattern.foreach; import static org.kl.pattern.DeconstructPattern.let; Figure figure = new Rectangle(); let(figure, (int w, int h) -> { System.out.println("border: " + w + " " + h)); }); matches(figure, Rectangle.class, (int w, int h) -> out.println("square: " + (w * h)), Circle.class, (int r) -> out.println("square: " + (2 * Math.PI * r)), Else.class, () -> out.println("Default square: " + 0) ); foreach(listRectangles, (int w, int h) -> { System.out.println("square: " + (w * h)); }); 

At the same time, to get a component, the class must have one or more deconstructing methods. These methods should be labeled Annotations Extract .
All parameters must be open. Since primitives cannot be passed to a method by reference, you need to use wrappers for IntRef, FloatRef, etc. primitives.


 import org.kl.type.IntRef; ... @Extract public void deconstruct(IntRef width, IntRef height) { width.set(this.width); height.set(this.height); } 

Also using Java 11, you can display types of deconstruction parameters.


 Figure figure = new Rectangle(); let(figure, (var w, var h) -> { System.out.println("border: " + w + " " + h)); }); matches(figure, Rectangle.class, (var w, var h) -> out.println("square: " + (w * h)), Circle.class, (var r) -> out.println("square: " + (2 * Math.PI * r)), Else.class, () -> out.println("Default square: " + 0) ); foreach(listRectangles, (var w, var h) -> { System.out.println("square: " + (w * h)); }); 

Property pattern allows you to simultaneously match the type and access the fields of the class by their names. Instead of providing all the fields, you can access the necessary and in any sequence.


 let (w: int w, h:int h) = figure; switch (figure) { case Rect(w: int w == 5, h: int h == 10) -> out.println("sqr: " + (w * h)); case Rect(w: int w == 10, h: int h == 15) -> out.println("sqr: " + (w * h)); case Circle (r: int r) -> out.println("sqr: " + (2 * Math.PI * r)); default -> out.println("Default sqr: " + 0); }; for ((w: int w, h: int h) : listRectangles) { System.out.println("square: " + (w * h)); } 

At the moment, the library supports the ability to check the values ​​of a variable with 3 types and decompose an object into 3 components.


 import org.kl.state.Else; import static org.kl.pattern.PropertyPattern.matches; import static org.kl.pattern.PropertyPattern.foreach; import static org.kl.pattern.PropertyPattern.let; import static org.kl.pattern.PropertyPattern.of; Figure figure = new Rectangle(); let(figure, of("w", "h"), (int w, int h) -> { System.out.println("border: " + w + " " + h)); }); matches(figure, Rect.class, of("w", 5, "h", 10), (int w, int h) -> out.println("sqr: " + (w * h)), Rect.class, of("w", 10, "h", 15), (int w, int h) -> out.println("sqr: " + (w * h)), Circle.class, of("r"), (int r) -> out.println("sqr: " + (2 * Math.PI * r)), Else.class, () -> out.println("Default sqr: " + 0) ); foreach(listRectangles, of("x", "y"), (int w, int h) -> { System.out.println("square: " + (w * h)); }); 

You can also use another method to simplify the naming of fields.


 Figure figure = new Rect(); let(figure, Rect::w, Rect::h, (int w, int h) -> { System.out.println("border: " + w + " " + h)); }); matches(figure, Rect.class, Rect::w, Rect::h, (int w, int h) -> out.println("sqr: " + (w * h)), Circle.class, Circle::r, (int r) -> out.println("sqr: " + (2 * Math.PI * r)), Else.class, () -> System.out.println("Default sqr: " + 0) ); foreach(listRectangles, Rect::w, Rect::h, (int w, int h) -> { System.out.println("square: " + (w * h)); }); 

Position pattern allows you to simultaneously match the type and check the value of the fields in the order of declaration. In Java, for this we need to first check the type, cast to the type, assign it to a new variable, and only then, through getters, access the fields of the class and check for equality.


 switch (data) { case Circle(5) -> System.out.println("small circle"); case Circle(15) -> System.out.println("middle circle"); case null -> System.out.println("Null value "); default -> System.out.println("Default value: " + data); }; 

At the moment, the library supports the ability to check the values ​​of a variable with 6 types and check 4 fields at once.


 import org.kl.state.Else; import org.kl.state.Null; import static org.kl.pattern.PositionPattern.matches; import static org.kl.pattern.PositionPattern.of; matches(data, Circle.class, of(5), () -> { System.out.println("small circle"); }, Circle.class, of(15), () -> { System.out.println("middle circle"); }, Null.class, () -> { System.out.println("Null value "); }, Else.class, () -> { System.out.println("Default value: " + data); } ); 

Also, if the developer does not want to check some fields, these fields should be marked with @Exclude annotations. These fields should be announced last.


 class Circle { private int radius; @Exclude private int temp; } 

The static pattern allows you to simultaneously type and deconstruct an object using factory methods.


 Optional some = ...; switch (some) { case Optional.empty() -> System.out.println("empty value"); case Optional.of(var v) -> System.out.println("value: " + v); default -> System.out.println("Default value"); }; 

At the moment, the library supports the ability to check the values ​​of a variable with 6 types and decompose an object into 3 components.


 import org.kl.state.Else; import static org.kl.pattern.StaticPattern.matches; import static org.kl.pattern.StaticPattern.of; Optional some = ...; matches(figure, Optinal.class, of("empty"), () -> System.out.println("empty value"), Optinal.class, of("of") , (var v) -> System.out.println("value: " + v), Else.class, () -> System.out.println("Default value") ); 

In this case, in order to obtain a component, the class must have one or more deconstructing methods, labeled Extract annotations.


 @Extract public void of(IntRef value) { value.set(this.value); } 

Sequence pattern makes it easier to process data sequences.


 List<Integer> list = ...; switch (list) { case Ranges.head(var h) -> System.out.println("list head: " + h); case Ranges.tail(var t) -> System.out.println("list tail: " + t); case Ranges.empty() -> System.out.println("Empty value"); default -> System.out.println("Default value"); }; 

Using the library can be written as follows.


 import org.kl.state.Else; import org.kl.range.Ranges; import static org.kl.pattern.SequencePattern.matches; List<Integer> list = ...; matches(figure, Ranges.head(), (var h) -> System.out.println("list head: " + h), Ranges.tail(), (var t) -> System.out.println("list tail: " + t), Ranges.empty() () -> System.out.println("Empty value"), Else.class, () -> System.out.println("Default value") ); 

You can also use the following functions to simplify the code.


 import static org.kl.pattern.CommonPattern.with; import static org.kl.pattern.CommonPattern.when; Rectangle rect = new Rectangle(); with(rect, it -> { it.setWidth(5); it.setHeight(10); }); when( side == Side.LEFT, () -> System.out.println("left value"), side == Side.RIGHT, () -> System.out.println("right value") ); 

As you can see, pattern matching is a powerful tool that makes writing code much easier. Using lambda expressions, references to the method, and the output of types of lambda parameters, it is possible to emulate the capabilities of pattern matching by the very means of the language.


The source code of the library is open and available on github .


')

Source: https://habr.com/ru/post/460773/


All Articles