Реализация сопоставления с образцом в Java

в 14:35, , рубрики: java, kotlin

Многие современные языки поддерживают сопоставление с образцом (pattern matching) на уровне языка. Java в данный момент не поддерживает pattern matching, но есть надежды что в будущем все может измениться.

Сопоставление с образцом раскрывают перед разработчиком возможность писать код более гибко и красивее, при этом оставляя его понятным.

Используя возможности Java 8, можно реализовать некоторые возможности pattern matching в виде библиотеки. При этом можно использовать как утверждение так и выражения.

Constant pattern позволяет проверить на равность с константами. В Java switch позволяет проверить на равность числа, перечисления и строки. Но иногда хочется проверить на равность константы объектов используя метод equals().

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);
};

В данный момент библиотека поддерживает возможность проверять значения переменной с 6 константами.

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)
);

Tuple pattern позволяет проверить на равность нескольких перемен с константами одновременно.

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 ");
};

В данный момент библиотека поддерживает возможность указывать 4 переменные и 6 веток.

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 позволяет одновременно сопоставить тип и извлечь значение переменной. В Java для этого нам нужно сначала проверить тип, привести к типу и потом присвоить новой переменной.

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);
};

В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами.

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 позволяет одновременно сопоставить тип и проверить на условия.

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);
};

В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами и условиями.

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); }
);

Для упрощения написания условия, разработчик может использовать следующее функции для сравнения: lessThan/lt, greaterThan/gt, lessThanOrEqual/le, greaterThanOrEqual/ge,
equal/eq, notEqual/ne. А для того чтобы опустить условия можно пременить: 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 позволяет одновременно сопоставить тип и разложить объект на составляющие. В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса.

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));
}

В данный момент библиотека поддерживает возможность проверять значения переменной с 3 типами и раскладывать объект на 3 составляющее.

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));
});

При этом чтобы получить составляющее, класс должен иметь один или несколько деконструирующих методов. Эти методы должны быть помечены аннотаций Extract.
Все параметры должны быть открытыми. Поскольку примитивы нельзя передать в метод по ссылке, нужно использовать обертки на примитивы IntRef, FloatRef и т.д.

import org.kl.type.IntRef;
...

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

Также используя Java 11, можно выводить типы деконструирующих параметров.

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 позволяет одновременно сопоставить тип и доступиться к полям класса по их именам. Вместо того, чтобы предоставить все поля, можно доступиться к нужным и в любой последовательности.

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));
}

В данный момент библиотека поддерживает возможность проверять значения переменной с 3 типами и раскладывать объект на 3 составляющее.

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));
});

Также для упрощения именования полей можно использовать другой способ.

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 позволяет одновременно сопоставить тип и проверить значение полей в порядке объявления. В Java для этого нам нужно сначала проверить тип, привести к типу, присвоить новой переменной и только тогда через геттеры доступиться к полям класса и проверить на равность.

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);
};

В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами и проверять сразу 4 поля.

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); }
);

Также если разработчик не хочет проверять некоторые поля, эти поля должны быть помечены аннотаций @Exclude. Эти поля должны быть объявлены последними.

class Circle {
      private int radius;

      @Exclude
      private int temp;
 }

Static pattern позволяет одновременно сопоставить тип и деконструировать объект используя фабричные методы.

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");
};

В данный момент библиотека поддерживает возможность проверять значения переменной с 6 типами и раскладывать объект на 3 составляющее.

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")
); 

При этом чтобы получить составляющее, класс должен иметь один или несколько деконструирующих методов, помеченные аннотаций Extract.

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

Sequence pattern позволяет проще обрабатывать последовательности данных.

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");
};

Используя библиотеку можно писать следующим образом.

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")
);   

Также для упрощения кода, можно использовать следующее функции.

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")
);

Как можно видеть pattern matching сильный инструмент, который намного упрощает написание кода. Используя лямбды-выражения, ссылки на метод и вывод типов параметров лямбды можно сэмулировать возможности pattern matching самыми средствами языка.

Исходной код библиотеки открыт и доступный на github.

Автор: koowaah

Источник


* - обязательные к заполнению поля