Pull to refresh

Comments 42

Pattern matching — функциональщина, притащенная «во многие языки» без понимания принципов, просто для того чтобы была лишняя фича, в рамках «развития языка». Притащенная соответственно функциональщиками, людьми, которые не знают что такое инкапсуляция.

Печально смотреть, как Java, хороший язык, со стройными изначально принципами ООП, все больше и больше превращают в PHP. Даже термин специальный придумали — деконструкция, ужас какой.
PHP просто как пример того как бездумное добавление новых возможностей в язык в итоге превращает его в непонятно что. Место PHP в этом примере легко займут и JavaScript, и Kotlin
Как ни странно, но ФП вполне сочетается с ООП. Даже в одном проекте.
Главное использовать подходящий к задаче в текущий момент.
Вообще-то нет, не сочетается, причем совсем. Вот рассмотрим «чудесный» паттерн декоструктор из статьи
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);
};

В ООП это было бы
out.println(String.format("square: %f", figure.square()));

или
figure.printSquare();

Но никак не так как в примерах из статьи. Тоесть, либо одно, либо другое, вместе никак.
Ну а если в программе возникает необходимость писать подобный код
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);
};

это значит лишь то что объектно-ориентированный дизайн зашел куда-то совсем не туда. Ну или это код внутри самой JDK, что врядли.
Совершенно независимо от качества примеров автора (и ограниченности данной библиотеки), вы похоже не понимаете, что такое pattern matching, и зачем он нужен.

>В ООП это было бы
>out.println(String.format(«square: %f», figure.square()));

Не было бы. Представьте на мгновение, что figure — это не ваши классы, что они написаны сторонним разработчиком, и менять (а то и посмотреть на их код) вы не можете.

>зашел куда-то совсем не туда
А даже если и зашел? Если часть этого кода не ваша, у вас мало шансов поменять там дизайн.

И да, то что тут рассмотрено — это библиотека, она не может превращать Java как язык во что-либо.
Совершенно независимо от качества примеров автора (и ограниченности данной библиотеки), вы похоже не понимаете, что такое pattern matching, и зачем он нужен.

Я прекрасно понимаю что это за паттерн и зачем он нужен. И что он принципиально не совместим с Java.
Не было бы. Представьте на мгновение, что figure — это не ваши классы, что они написаны сторонним разработчиком, и менять (а то и посмотреть на их код) вы не можете.

В этом случае тем более никак и никогда не могу использовать паттерн матчинг, потому что все что я вижу из библиотеки — это интерфейсы. я не вижу внутренностей объектов. Попытка использовать тяжелую артиллерию вроде рефлекшена для того чтобы подсмотреть внутренности объектов в библиотеке — это ядерная бомба замедленного действия. В следующей версии библиотеки автор поменяет свои объекты, и весь паттерн матчинг рухнет как карточный домик.
Если я хочу что-то добавить к коду библиотеки, я оборачиваю библиотечные классы своими классами, но никак не лезу рефлекшеном внутрь библиотечных классов.
Вы просто не понимаете суть ООП, если для вас добавление чего-то к библиотечному коду это обязательно означает разобрать объект на кусочки.

Простите, а как связаны pattern matching и залезание рефлексией внутрь чужих объектов?

>Я прекрасно понимаю что это за паттерн и зачем он нужен.
Совершенно не похоже. Вы бы хоть википедию что-ли прочитали? В тамошней вполне приличной формулировке матчинг — это поиск паттерна в последовательности токенов . Не в объекте — это лишь частный случай, а в последовательности объектов. Типичный случай — дерево (ну или список из головы и хвоста).
примеры из статьи — искуственные и показывают что можно делать (но отнюдь не что нужно).

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

те же конечные автоматы гораздо удобнее получаются.

еще хорошо для _лаконичных_ деконструкций. когда метод возвращает например два значения — и распаковка pair в две отдельных переменных это тоже сопоставление с образцом. особенно если нужна только одна из них дальше.

да, всегда можно написать иначе. ну на то и сахар, чтоб вкуснее было.
Не было бы. Представьте на мгновение, что figure — это не ваши классы, что они написаны сторонним разработчиком, и менять (а то и посмотреть на их код) вы не можете.

это делается через визитор. Вот такие свичи это практичеси всегда быдлокод.
в конструировании потока управления данными,

не очень оптимально использовать рефлекшн, потому что он дольше чем обычный доступ к переменным. Тут прикручивается стирание классов например Питона к яве. Так берите Питон для этой задачи.

> это делается через визитор. Вот такие свичи это практичеси всегда быдлокод.


Визитор это и есть такой свитч, только obscured.

быдлокод в том что ты делаешь ветку вручную в то время как то же самое сделает для тебя компилятор такой фичей ОО как полиморфизм функций. Кроме того ручной свич работает в жаюе медленнее чем вызов функций — давным давно протестил. Я согласен что следует ввести другой синтаксис для визитора а не как сейчас через зад. Но это не оправдывает свич.

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

Быдлокод оно именно потому, что язык не поддерживает матчинг как следует — например, не отлавливает, все ли возможные случаи были рассмотрены.

Ну и да, визитор — это неудобный (частный, для ООП) способ реализации матчинга, если угодно.
примеры из статьи — искуственные и показывают что можно делать (но отнюдь не что нужно).

Приведите нормальный пример, который доказывает нужность этой концепции и этой библиотеки. Пока что все абсолютно примеры доказывают её вредность и ненужность.
Ну может вот этот только можно использовать
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 ");
};
сопоставление с образцом используется в конструировании потока управления данными, например. писать отдельный класс под каждый промежуточный вариант — та еще тухлятина.

Сопоставление с образцом используется в функциональных языках, потому что в них нет понятия инкапсуляции и типов (в том понимании в котором они используются в Java например).
В Java да, именно что под каждый промежуточный вариант надо делать свой класс. Так принято в Java. Но ненадо в Java тащить чужеродные концепции. Если не умеете программировать на Java, и очень хочется использовать pattern matching — идите в Haskel, Lisp, Scala и т.п.
Перевожу что вы сказали: «не трогайте то к чему я привык».
ОК, вам не нужен синтаксический сахар.
Я знаю как жавистам больно от введения «var».
Простите. Придётся учиться, энтерпрайз не повод продолжать писать как 50 лет назад.
>Я знаю как жавистам больно от введения «var».
Вот не надо обобщать. Это мнение одного человека, не более того. У меня вот энтерпрайз в чистом виде — и никто даже не задумается, применять ли матчинг, если он вдруг появится в Java (в Scala — уже).
Вообще по статье: гораздо интереснее читать как это было реализовано, какие накладыне расходы, что с совместимостью, какие требования.

Препроцессор какой-нибудь, типа ретролямбд, скорее всего.

тогда откуда странное ограничение на число веток? :)
тут явно напрашивается «ненормальное программирование» в хабы и хотел бы прочесть как человек дошел до такой жизни.
Поскольку для разных вариантов параметров лямбды используются перегрузки метода matches().
Было бы неплохо сравнить скажем с vavr, где почти все тоже самое уже есть довольно давно.
Motif библиотека использует Java Stream style.
Эта библиотека предоставляет более простой и подобный к стилю написанию C# pattern matching. Не нужно писать when(), get() и другие методы.
только это не паттерн матчинг а инстанс валидатор.
короче вроде универсального визитора? Достаёт значения через рефлекшн и конструирует реальные инстанции обьекта и сравнивает по equals? Ну так джава код и не предназначет для массивной сериализации инстанций)))
боже какие замороты с тем что в питоне есть просто так.

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

есть также принцип разделения функций. Я тоже взглянув с перва подумал что это быдлокод со свичами, заменяющими полиморфизм, но тут можно выделить произвольную проверку в отдельный класс — принцип визитора

И кстати много кода не нужно. Тут в один небольшой класс поместится весь функционал вышеперечисленного, если с рефлекшном, а если без, то как вы достаёте поля?

А всё, на гитхабе кто посмотрит тот поймёт что достают рефлекшном например через fields. Можно делать одним классом вроде кода ниже только расширить.
package utils;

import  java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

public class AnyValidator {
	
	private Class lastClass = null;
	
	private String lastResult = "";
	
	private Object dummy = new Integer(10);
	
	
	
	
	public boolean validateFields(Object instance, Map<String, Object> mp) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
		
		lastClass = instance.getClass();
		
		Iterator<Map.Entry<String, Object>> it = mp.entrySet().iterator();
		
		while (it.hasNext()) {
			
			Map.Entry<String, Object> entry = (Map.Entry<String, Object>)it.next();
			
			String fieldName = entry.getKey();
			
			Object goodValue = entry.getValue();
			
			boolean isGoodField = equalsObjectField(fieldName, instance, goodValue);
			
			if (! isGoodField) {
				
				lastResult = "validateFields: field " + fieldName + " is wrong";
				return false;
			}
			
			
		}
		return true;
		
		
		
	}
	
	public boolean equalsObjectField(String fieldName, Object instance, Object goodValue) throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException {
	
		Field field = lastClass.getDeclaredField(fieldName);
		
		Object  fieldValue = field.get(instance);
		
		if (goodValue == null)
				return fieldValue == null;
		
		return goodValue.equals(fieldValue);
		
		
	}
	
	
	
	
	public static void main(String[] args) {
		
		AnyValidator dummy = new AnyValidator();
		
		AnyValidator val = new AnyValidator();
		
		Map<String, Object> mp = new HashMap<String, Object> ();
		
		mp.put("dummy" ,  new Integer(10));
		
		mp.put("lastResult" ,  "");
	
		mp.put("lastClass" ,  null);
		
		
		
		
		try {
			boolean res = val.validateFields(dummy, mp);
			
			System.out.println(res);
			
			dummy.dummy = new Integer(11);
			
			res = val.validateFields(dummy, mp);
					
			System.out.println(res);
			
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
		
	}

}




Никакой магии под капотом нету. Используется рефлексия где нужно и те же if/else.
Ограничения на количество веток вызвано тем что, что для каждого случая используется разные перегрузки функции matches().
По сравнению с if/else цепочкой, matches() методы под капотом делает тоже, но где нужно используют рефлексия.
Накладные расходы добавляются с рефлексией, но без неё не как.
Написано на чистой Java 8.

Не рассматривали, в качестве оптимизации, замену рефлексии возможностями java.lang.invoke, а, особенно, LambdaMetaFactory, т.е. генерацию на лету для вызовов методов реализаций неких функциональных интерфейсов? + кэширование соответствующих вещей
Допустим, в Вашем DeconstructPattern вызов verifyExtractMethods(..) основывается на (для каждого вызова) анализе класса для поиска аннотированных методов (takeExtractMethods(..)) и дальнейшем их вызове через открытие доступа и рефлективый вызов. Вместо этого же можно было результат поиска метода кэшировать, причём для вызова самого метода генерируя объект функ. интерфейса, а ля


interface Extractor {
    void extract(Object... parameters);
} 

или даже JDKшный Consumer<Object[]>,
что позволило бы избавиться от постоянных накладных расходов на открытие доступа к рефлективной сущности, да и сделало код более явным.


Как по мне, учитывая то, что эта библиотека, в первую очередь, синтаксический сахар, вопрос производительности тут критичен, потому что, даже принимая во внимание магию JIT, сахар с сравнительно большим оверхедом проигрывает более многословным, но примитивных, с точки зрения генерируемого байткода, способам достижения такой же логики.


Если же углубляться в оптимизации, то можно попробовать генерировать в рантайме полностью свои сущности для доступа к каким-то вещам, или даже, используя Intrumentation API, при наличии соответствующего агента, заменять при загрузке засахаренных классов вызовы сложных методов байткодом подобного же, но без сахара (e.g, для всё того же Deconstructor'а производить, фактически, инструкции доступа к полям.


В целом же необычных подход к добавлению сахара :)


При вашем желании, могу накидать Issues на гитхабе

Идею использования java.lang.invoke, а, особенно, LambdaMetaFactory не рассматривал для оптимизации. Но обязательно попробую рассмотреть этот вариант.

Можете более детально описать как именно можно закэшировать, вместо прямого поиска и вызова метода через рефлексию?

На гитхаб можете писать Issues, буду рад.
Можете более детально описать как именно можно закэшировать, вместо прямого поиска и вызова метода через рефлексию?

Что касается кэша, то, с точки зрения реализации, тут варианты любые, в зависимости от ваших же предпочтений (JDK: Map, Guava: Cache и т.д.). Я, скорее, говорил о подходе, что, учитывая, что у вас зачастую происходит вызов методов с одинаковыми параметрами, для которых происходит полный анализ класса, уместно результат этих самых вычислений сохранять для дальнейшего переиспользования.
Для примера, тот самый <V> List<Method> takeExtractMethods(V value)уместно заменить на List<Method> getExtractMethods(Class<?> value) (простите мне моё переименование, не позволяет тут совесть take оставить :) ) (кстати, генерик тоже тут излишен), который будет анализировать содержимое класса (ваше нынешнее тело метода) только в том случае, если в кэше для данного класса это ещё не найдено, после чего это значение будет заноситься в этот самый кэш, дабы при повторном вызове не пришлось производить эту затратную операцию. С этой точки зрения особенно хороша, как по мне, реализация Guava, так как она и функциональна (V get(K, Callable<? extends V>), где K и V типы ключей и значений соотв-но) и позволяет удобно настраивать режим освобождения кэша.


На гитхаб можете писать Issues, буду рад.

Отлишно, постараюсь накидать в ближайшее время

Ок. Попробую реализовать кеш аналогичен Guava Cache.
И внедрить в проект.
В паттернах PropertyPattern, PositionPattern используется доступ к полям (Field) через рефлексию и считывания значения. Может быть тоже сделать кеширования? И доступ к полю Field.get() сделать через LambdaMetaFactory тоже.

Что касается идеи, в целом — да, я просто описал это в общем (так это у вас не в одном месте может быть полезно).
Что касается доступа к полям, тут можно улучшить производительность через Invoke API, но LambdaMetaFactory использовать не получится, так как её реализация в OpenJDK (и, полагаю, иных тоже) не допускает генерацию классов для инструкций доступа к полям (инструкции putfield, putstatic, getfield, getstatic). Поэтому тут получится лишь создавать MethodHandle, который, однако, также показывает незначительный прирост в производительности.

Может быть с кешированием использовать VarHandle, вместо MethodHandle?
Но поскольку VarHandle доступны в Java 9 и выше, то делать с проверкой. Если Java 8 — MethodHandle, а если выше — VarHandle.
Вдохновениям для реализации библиотеки для паттерн матчинга стал С#. В С# 7,8 добавлены почти все эти паттерны. Как можно видеть паттерн матчинг прекрасно вписывается в С#.

З другой стороны паттерн матчинг является одной с приоритетных направлений Java.
Есть уже два jep:
pattern matching for instanceof.
pattern matching for switch.

Но ждать прийдется какия минимум к LTS Java 17 а то и больше.
З другой стороны паттерн матчинг является одной с приоритетных направлений Java.

думайте что хотите, для кого приоритетное, для кого — антипаттерн, и ту задачу в бизнес логике которую вы прикручиваете этим матчингом можно сделать другим способом без него более чистым кодом.
Доступ к полям практично делать аннотациями как в Spring, причём подозреваю так как функционал спринга довольно широк там есть решение для этого матчинга причём более чистое.
Когда ввели лямбды-выражения во многих людей тоже были пессимистические настроения. Мол зачем они в Java, если Java чистый объектно-ориентированный язык. А сейчас без них и не представишь разработку.
Я думаю тоже будет и с паттерн матчингом.
Sign up to leave a comment.

Articles