Ява Карри
Демонстрация каррирования на языке программирования Java.
Карри-функция - это функция, которая принимает n аргументов, из которых уже заполнено до n – 1 аргументов. Думайте об этом как о приготовлении риса с карри. Одно карри - это не блюдо, а большая его часть. Вы добавляете оставшиеся ингредиенты и готово. Проведя такую замечательную аналогию, вы могли бы подумать, что это происхождение термина, но нет, он назван в честь математика и логика Хаскелла Карри. Сам Карри считал математика и логика Моисея Шенфинкеля первым человеком (в 1923 году), который использовал эту концепцию, и что он только независимо открыл ее.
Итак, вернемся к каррированию. Рассмотрим пример. Мы хотим измерить время, за которое камень упадет с разной высоты. Для простоты мы не будем учитывать влияние сопротивления воздуха. Предположим, что высота камня представлена переменной s
, а ускорение свободного падения - переменной g
. Затраченное время просто +√2√s/√g
. Проще говоря, это положительный квадратный корень из расстояния, деленный на положительный квадратный корень из ускорения свободного падения.
Ниже приведен простой пример кода на Python 3.
def time_taken(s, g): return (2 * s / g) ** 0.5
Мы определяем функцию с именем time_taken
, которая принимает два параметра, s
и g
. Функция делит s
на g
и возвращает их квадратный корень. Достаточно просто, в чем проблема?
Предположим, что теперь вы хотите предоставить функцию, которая измеряет, сколько времени потребуется, чтобы упасть с фиксированной высоты на различные астрономические тела. Вам нужен простой способ измерить время, которое потребуется, чтобы упасть на сотню метров с Земли, Марса, Венеры, Солнца, Луны, спутников Юпитера, Ганимеда, Ио и т. Д. И, как программист, вы не хотите каждый раз передавать значение 100 функции, потому что это повторяется и скучно. Вы хотите, чтобы функция называлась (возможно, плохо) time_taken_100
. Ниже представлен простой способ добиться этого:
def time_taken_100(g): return time_taken(100.0, g)
Поздравляю, вы только что карри выполнили функцию time_taken
. Конечно, на этом не нужно останавливаться. Вы могли бы так же легко написать функцию time_taken_earth
, которая, учитывая расстояние, вычисляет время, необходимое для того, чтобы объект упал на землю. Это было бы так просто:
def time_taken_earth(s): return time_taken(s, 9.8)
Легко представить, насколько полезна такая возможность. Такие языки, как Haskell, частично предлагают эту функцию бесплатно. (Да, язык назван в честь Haskell Curry.)
Что для этого нужно сделать на языке программирования Java?
Ниже представлена простая программа, демонстрирующая каррирование на Java. Обратите внимание, что для наглядности я добавил номера строк к каждой строке. Хотя это отлично подходит для пояснительных целей, копирование кода становится утомительным занятием. Для запуска этого кода (настоятельно рекомендуется) весь код, показанный в этой статье, доступен как суть .¹
1 import java.util.function.Function; 2 3 public class Currier0 { 4 public static String concat(String s, Integer i, Integer j) { 5 return s + i + j; 6 } 7 8 public static 9 Function<String, 10 Function<Integer, 11 Function<Integer, String>>> curry() { 12 return s -> (i -> (j -> concat(s, i, j))); 13 } 14 15 public static void main(String[] args) { 16 String direct = concat("hello", 12345, 67890); 17 System.out.println(direct); // prints hello1234567890 18 19 String curried = curry() 20 .apply("hello") 21 .apply(12345) 22 .apply(67890); 23 System.out.println(curried); // prints hello1234567890 24 } 25 }
Помимо метода main
, он определяет два метода, concat
и curry
, определенные в строках 4 и 11 соответственно. Метод concat
принимает три аргумента: String
и два Integer
. Он объединяет их и (благодаря автоматическому преобразованию типов в Java) возвращает объект String
. Пример вызова concat
показан в строке 16, где результат сохраняется в переменной direct
. Его результат выводится на консоль в строке 17 и также отображается в комментарии. Он печатает hello1234567890
.
Тело функции curry
почти так же лаконично, как и сам метод concat
. Он также делегирует concat
методу для фактической обработки. Во всяком случае, тип возврата curry
- это то, что сложно. Он возвращает (задержите дыхание) функцию, которая принимает строку и возвращает функцию, которая принимает целое число и возвращает функцию, которая принимает целое число и возвращает строку. Если вы поняли это с первого раза, похлопайте себя по плечу. Вам, вероятно, не нужен этот пост. Читателям, знакомым с типизированными языками функционального программирования, такими как Haskell, ML, OCaml, синтаксис Java покажется знакомым. Фактически, тело метода в curry
—s -> (i -> (j -> ...))
- выглядит точно так же, как и в этих языках, за исключением того, что здесь скобки будут излишними, потому что функции естественно правильно ассоциативны. В Java явная группировка является обязательной по синтаксическим причинам.
Чтобы лучше понять, что делает карри, можно его разобрать. Попробуйте запустить следующие варианты:
... elided ... public static void main(String[] args) { Function<String, Function<Integer, Function<Integer, String>>> curried = curry(); System.out.println(curried); System.out.println(curried.apply("e")); System.out.println(curried.apply("e").apply(27)); System.out.println(curried.apply("e").apply(27).apply(18)); }
В зависимости от вашего jdk ваш вывод будет выглядеть примерно так, как показано ниже. Обратите внимание, что последний вывод всегда должен быть одинаковым.
Currier0$$Lambda$1/0x0000000800060840@64b8f8f4 Currier0$$Lambda$2/0x0000000800062840@1996cd68 Currier0$$Lambda$3/0x0000000800062c40@555590 e2718
Выше - результат openjdk11. Обратите внимание, что первые 3 строки - это все лямбды Java. Что происходит, так это то, что каждый вызов переменной curried
возвращает другую лямбду до тех пор, пока, наконец, когда он не получит все необходимые переменные, он делегирует исходному методу (concat
, в данном случае) и не вернет результат (e2718
, в данном случае) .
Следующий шаг Карри: программный скелет
Надеюсь, шаги пока ясны. Хотя Currier0.java делает свое дело и прост, он также неудовлетворителен. Легко написать a -> b -> c -> ... -> m -> doIt(a, b, c, ..., m)
, когда мы уже знаем a, b, ..., m
, что нам действительно нужно - это программно сгенерировать карри.
Начнем медленно. Ранняя абстракция, как и преждевременная оптимизация, - это корень многих зол. Ниже представлен Currier1.java. Его функция каррирования называется curry1
(строка 17). Он принимает java.lang.reflect.Method
, из которого программно генерирует каррированную функцию. Мы обсудим детали после блока кода ниже.
1 import java.lang.reflect.InvocationTargetException; 2 import java.lang.reflect.Method; 3 import java.util.function.Function; 4 5 public class Currier1 { 6 public static final class Distance { 7 public double distance(double t, double v, double u) { 8 return v * t + 0.5 * u * t * t; 9 } 10 } 11 12 public static String concat(String s, int i, int j) { 13 return s + i + j; 14 } 15 16 @SuppressWarnings("rawtypes") 17 static Function curry1(Method method) { 18 return (Function) (Object s) -> { 19 Object self = s; 20 Object[] args = new Object[3]; 21 return (Function) (Object i) -> { 22 args[0] = i; 23 return (Function) (Object j) -> { 24 args[1] = j; 25 return (Function) (Object k) -> { 26 args[2] = k; 27 try { 28 return method.invoke(self, args); 29 } catch (IllegalAccessException e) { 30 throw new RuntimeException(e); 31 } catch (InvocationTargetException e) { 32 throw new RuntimeException(e.getCause()); 33 } 34 }; 35 }; 36 }; 37 }; 38 } 39 40 public static void main(String[] args) throws Exception { 41 Method concatM = Currier1.class.getMethod( 42 "concat", String.class, int.class, int.class); 43 @SuppressWarnings("unchecked") 44 Function<Void, 45 Function<String, 46 Function<Integer, 47 Function<Integer, String>>>> curry1 = curry1(concatM); 48 String curried1static = curry1 49 .apply(null) // static methods take no instance 50 .apply("hello") 51 .apply(12345) 52 .apply(67890); 53 54 System.out.println(curried1static); // prints hello1234567890 55 56 Method distM = Distance.class.getMethod( 57 "distance", double.class, double.class, double.class); 58 @SuppressWarnings("unchecked") 59 Function<Distance, 60 Function<Double, 61 Function<Double, 62 Function<Double, Double>>>> instanceCurry = curry1(distM); 63 Distance cm = new Distance(); 64 Double distance = instanceCurry 65 .apply(cm) 66 .apply(1.0d) // t 67 .apply(2.0d) // v 68 .apply(10.0d); // u 69 System.out.println(distance); // prints 7.0 70 } 71 }
Метод curry1
принимает класс отражения Java, Method
, и из него генерирует Function
. Вкратце о том, как работает метод. Он представляет метод, поведение которого мы хотим динамически выполнять. Любой метод Java обычно вызывается как instance.dwim(arg1, arg2, arg3, .., argN)
, где instance
- это объект, который вызывает метод dwim
(сокращение от делать то, что я имею в виду ²) с его аргументами. Чтобы Method функционировал (это не каламбур), он немного меняет этот порядок. Класс Method имеет метод с именем invoke
со следующей сигнатурой
public Object invoke(Object obj, Object… args)
³
Где, obj
- объект, из которого вызывается базовый метод; и args
- аргументы, используемые для вызова метода.
Таким образом, вместо obj.dwim(arg1, arg2, ..., argN)
, учитывая метод m
, вы скажете m.invoke(obj, new Object[] { arg1, arg2, ..., argN })
. ⁴
Тогда вернемся к функции curry1
. Первый вызов curry1
(строго говоря, вызов первой функции, возвращаемый curry1
, но я буду использовать более разговорные термины в остальной части этого текста), это «объект, лежащий в основе метода. вызывается из ». Мы сохраняем его в переменной с именем self
. Все последующие вызовы сохраняются в массиве Object
. Для простоты в этой итерации (Currier1) мы используем фиксированный массив размера 3.
Ниже я повторил curry1
, номера строк и все остальное с добавленными аннотациями. На каждом этапе - строки 18, 21, 23, 25 и 28 - мы возвращаем лямбда-функцию Java, представляющую каждое каррированное состояние. Method::invoke
выдает несколько проверенных исключений, поэтому у нас есть код для их обработки
17 static Function curry1(Method method) { 18 return (Function) (Object s) -> { 19 Object self = s; 20 Object[] args = new Object[3]; 21 return (Function) (Object i) -> { 22 args[0] = i; // save first argument 23 return (Function) (Object j) -> { 24 args[1] = j; // save second argument 25 return (Function) (Object k) -> { 26 args[2] = k; // save third argument 27 try { 28 return method.invoke(self, args); // invoke! 29 } catch (IllegalAccessException e) { 30 throw new RuntimeException(e); 31 } catch (InvocationTargetException e) { 32 throw new RuntimeException(e.getCause()); 33 } 34 }; // these close braces look like lisp code 35 }; 36 }; 37 }; 38 }
Чтобы показать, что curry1
достаточно универсален для всех методов, которые принимают 3 аргумента, у нас есть два образца в нашем коде (повторяются ниже). Один - это метод concat
(строка 12), скопированный из Currier0, а другой - метод distance
из пользовательского класса Distance
(строка 6). Эти два примера показывают, что мы можем каррировать как статические методы, так и методы экземпляра (методы, которые требуют, чтобы объект работал). Мы посмотрим, как они каррированы и вызываются после поля кода ниже.
6 public static final class Distance { 7 public double distance(double t, double v, double u) { 8 return v * t + 0.5 * u * t * t; 9 } 10 } 11 12 public static String concat(String s, int i, int j) { 13 return s + i + j; 14 }
Ниже показан метод main класса. Сначала посмотрим на тот же пример, что и Currier0, concat
. В строке 41 мы рефлексивно извлекаем метод. В строке 47 мы фиксируем каррированный экземпляр в переменной с именем curry1
. Обратите внимание, что благодаря автоматическому определению типа в Java мы фиксируем возвращаемый тип. (Обратите также внимание на то, что, поскольку в Java отсутствуют овеществленные дженерики, она с радостью присвоит переменной любую заданную спецификацию типа и взорвется во время выполнения, если вы сделаете ошибку.) В строках 48–52 мы вызываем каррированный метод. Аргументы метода должны быть знакомы по Curried0, разница здесь в спецификации obj
. Поскольку concat
- статический метод, мы передаем null
. (В спецификации типа в строке 44 он также указан как Void
.) Мы рассмотрим вызов метода экземпляра после просмотра всего кода main
метода.
40 public static void main(String[] args) throws Exception { 41 Method concatM = Currier1.class.getMethod( 42 "concat", String.class, int.class, int.class); 43 @SuppressWarnings("unchecked") 44 Function<Void, 45 Function<String, 46 Function<Integer, 47 Function<Integer, String>>>> curry1 = curry1(concatM); 48 String curried1static = curry1 49 .apply(null) // static methods take no instance 50 .apply("hello") 51 .apply(12345) 52 .apply(67890); 53 54 System.out.println(curried1static); // prints hello1234567890 55 56 Method distM = Distance.class.getMethod( 57 "distance", double.class, double.class, double.class); 58 @SuppressWarnings("unchecked") 59 Function<Distance, 60 Function<Double, 61 Function<Double, 62 Function<Double, Double>>>> instanceCurry = curry1(distM); 63 Distance cm = new Distance(); 64 Double distance = instanceCurry 65 .apply(cm) 66 .apply(1.0d) // t 67 .apply(2.0d) // v 68 .apply(10.0d); // u 69 System.out.println(distance); // prints 7.0 70 } 71 }
Если вы понимаете статический метод, метод экземпляра не должен сильно отличаться. В строках 56–57 мы получаем метод distance
. Мы фиксируем карри в строках 59–62 в переменной instanceCurry
. В отличие от статического метода, мы создаем экземпляр класса Distance
в строке 63 и передаем его карри. Строки 64–68 показывают вызов карри. Это то же самое, что и со статическим методом, за исключением других типов и, конечно же, другого результата.
Карри 2: универсальное решение
Curried1 - большой шаг вперед по сравнению с Curried0. Он обрабатывает все произвольные методы, которые принимают ровно 3 аргумента. В следующей итерации Curried2 мы карризуем все методы. Ниже приведен полный код. В этой итерации я не буду использовать concat
или main
. Обе они достаточно похожи на две предыдущие версии. Что интересно, так это метод curry2
. Давайте посмотрим на это после полного фрагмента кода.
1 import java.lang.reflect.InvocationTargetException; 2 import java.lang.reflect.Method; 3 import java.util.function.Function; 4 import java.util.function.Supplier; 5 6 public class Currier2 { 7 public static String concat(String s, Integer i, Integer j) { 8 return s + ":" + i + ":" + j; 9 } 10 11 @SuppressWarnings("unchecked") 12 public static void main(String[] args) throws Exception { 13 Method concatM = Currier2.class.getMethod( 14 "concat", String.class, Integer.class, Integer.class); 15 Function<Currier2, 16 Function<String, 17 Function<Integer, 18 Function<Integer, String>>>> curry2 = curry2(concatM); 19 String concatted = curry2 20 .apply(null) 21 .apply("hello") 22 .apply(12345) 23 .apply(67890); 24 25 System.out.println(concatted); // prints hello:12345:67890 26 } 27 28 @SuppressWarnings("rawtypes") 29 public static Function curry2(Method method) { 30 int parameterCount = method.getParameterCount(); 31 32 Function f = o -> { 33 Object self = o; 34 Object[] args = new Object[parameterCount]; 35 36 Supplier<?> c = () -> { 37 try { 38 return method.invoke(self, args); 39 } catch (IllegalAccessException e) { 40 throw new RuntimeException(e); 41 } catch (InvocationTargetException e) { 42 throw new RuntimeException(e.getCause()); 43 } 44 }; 45 46 if (parameterCount == 0) { 47 return c.get(); 48 } 49 50 Function[] fns = new Function[parameterCount]; 51 52 for (int i = 0; i < parameterCount - 1; ++i) { 53 int j = i; 54 fns[i] = v -> { 55 args[j] = v; 56 return fns[j + 1]; 57 }; 58 } 59 60 fns[parameterCount - 1] = a -> { 61 args[parameterCount - 1] = a; 62 return c.get(); 63 }; 64 65 return fns[0]; 66 }; 67 68 return f; 69 } 70 }
curry2
выглядит иначе, но во многом такой же, как curry1
. Разница в том, что мы не можем заранее указать, сколько аргументов ожидает метод. Это может быть от 0 до 255 «». 255 - это довольно небольшое число, и мы могли бы просто switch/case
его ввести, но что в этом интересного?
Во-первых, в строке 30 мы фиксируем, сколько параметров нужно методу. Сохраняем его в переменной parameterCount
. Далее мы подготавливаем функцию f
. Мы вернем эту переменную. Как и все java.util.function.Function
, он ожидает одного параметра. Первым параметром всегда является вызывающий объект. Мы сохраняем это как Object self
в строке 33. Строка 34 создает массив объектов args
, представляющий все аргументы, необходимые методу. Если method
не ожидает аргументов, parameterCount
будет 0
, а args
будет пустым массивом.
Строки 36–44 выглядят немного неуместно, поэтому я потрачу немного времени на их объяснение. Supplier
- это функциональный интерфейс, представленный в Java 8. Его абстрактный метод get
не принимает аргументов и возвращает значение. Его можно вызывать несколько раз. В нашем случае поставщик фиксирует способ (из-за отсутствия лучшего термина) вызова Method::invoke
. Он лексически захватывает method
, self
и args
, поэтому может по запросу выполнять Method::invoke
. Кроме того, он также обрабатывает проверенные исключения, выданные Method::invoke
.
Назначение поставщика объясняется предложением if
в строке 46. В случаях, когда method
не требует аргументов и создается экземпляр self
, мы должны просто вернуть результат вызова метода. И это то, что мы делаем в строке 47.
Если для method
требуется более одного аргумента, теперь мы создаем массив из Function
(строка 50), каждый из которых вызывает функцию, следующую в строке⁶ - строки 52–58. Последняя функция (строки 60–63) вызывает Supplier::get
. Первая обязанность определения каждой функции - захватить ее параметр и сохранить его в args
- строки 55, 61. На первый взгляд бесполезное присвоение в строке 53, int j = i;
, необходимо, потому что все переменные, на которые есть ссылки в лямбдах, должны быть фактически final . Таким образом можно сказать, что ссылка никогда не должна изменяться в течение всего срока ее существования, несмотря на то, что ее значение может измениться. (В этом случае, поскольку i
является примитивом, ссылка отсутствует, поэтому он не должен вообще изменяться.)
Благодаря этому у нас есть полностью общий класс каррирования. Мы почти закончили.
Три этапа карри
Давайте сделаем короткую остановку, чтобы обобщить то, что мы узнали о каррировании реализаций.
Все реализации каррирования состоят из трех этапов: начального, промежуточного и конечного.
Первый вызов метода карри - это начальный этап. В нем вызывающие передают нам информацию о том, кто вызывает карри. Это null
для методов и obj
для методов экземпляра. Промежуточные этапы включают захват всех аргументов, необходимых для каррированной функции, а заключительные этапы представляют собой фактический вызов каррированной функции. Обычно заключительный этап включает в себя как захват параметра, так и выполнение вызова. (Потому что в противном случае вызов карри вместо curry.apply(1).apply(2).apply(n)
будет выглядеть как curry.apply(1).apply(2).apply(n).accept()
, а этого у нас не может быть!)
Итак, мы готовы к ...
Карри 3: общий, ленивый, разделяемый, с выводом типов, исполняемый и потокобезопасный
Currier2 великолепен. Он работает во всех практических целях. Что не хватает?
Хотя я придерживаюсь своей рекомендации в README: Никакая часть этого на самом деле не рекомендуется для использования в производственной системе, все же было бы неправильно писать небезопасный для потоков код. Вдобавок внимательные читатели заметили бы, что нет принципиальной причины исключать конструкторы из смеси карри (хорошо, небольшая игра слов).
Поскольку Currier3.java состоит из 156 строк, я не печатаю его полностью. В духе разработки, управляемой тестированием (TDD), давайте сначала посмотрим на скелет файла, а затем на метод main
(который имеет тесты), а затем рассмотрим его реализацию.
Как и более ранние методы карри, в этом также используется статический фабричный метод. Вы вызываете curry3
с Method
(строки 17–19) или с Constructor
(строки 21–24). К счастью для нас, Java 1.8 представила абстрактный класс Executable как родительский для метода и конструктора, что упростило нашу реализацию.
Класс FunctionE
(Функция с окружением E) реализует интерфейс Java Function
. Само это объявление показано в строке 26. Мы рассмотрим его подробно сейчас.
Метод main
должен быть знаком по предыдущим версиям. Единственная разница - это карринг конструктора объекта, чтобы отображался только он. Обратитесь к сути за подробностями. В строке 145 мы находим конструктор String, который принимает byte[]
и Charset
. Мы преобразуем его в переменную c3cons
в строках 147–149 и выполняем. Для его выполнения мы используем некоторую магию байтового кодирования для чтения одной строки в одной кодировке (ISO-8859–1) и вывода в другой (UTF-8). В выводе используются символы Unicode, которые (imo) читаются как карри. Кодирование здесь не обсуждается, но оставлено в качестве упражнения.
1 import static java.nio.charset.StandardCharsets.ISO_8859_1; 2 import static java.nio.charset.StandardCharsets.UTF_8; ... remaining imports elided ... 16 public final class Currier3 { 17 public static <T, R> Function<T, R> curry3(Method method) { 18 return new FunctionE<>(method); 19 } 20 21 public static <T, R, K extends T> Function<T, R> 22 curry3(Constructor<K> constructor) { 23 return new FunctionE<>(constructor); 24 } 25 26 private static final class FunctionE<T, R> implements Function<T, R> { ... described later ... 101 } 102 103 public static void main(String[] args) throws Exception { ... 105 ... other examples elided ... ... 145 Constructor<String> stringConstructor = String.class.getConstructor( 146 byte[].class, Charset.class); 147 Function<String, 148 Function<byte[], 149 Function<Charset, String>>> c3cons = curry3(stringConstructor); 150 String byteArray = c3cons 151 .apply(null) 152 .apply("ââªâ¡âÒ¯".getBytes(ISO_8859_1)) 153 .apply(UTF_8); 154 System.out.println(byteArray); // prints ⊂∪ⓡ╓ү 155 } 156 }
Это должно показаться достаточно знакомым для более ранних версий, так что, надеюсь, все ясно.
Теперь посмотрим на сам класс FunctionE
. Это тоже будет достаточно похоже на реализацию в Currier2. Различия здесь в том, что мы явно реализуем интерфейс Function, поддерживаем состояние и ничего не меняем. Во-первых, переменные экземпляра:
Executable executable
- захватывает объект метода или конструктора. Мы контролируем настройку только через общедоступные конструкторы (вызываемые фабричными методами, поскольку сам класс является частным). Явные общедоступные конструкторы сохраняют будущий код кода. В сегодняшней версии Java класс Executable имеет только два подкласса - Method и Constructor. Мы не можем предсказать, что так будет всегда, и без такой гарантии два конструктора позволяют нам поддерживать только те случаи, о которых мы знаем.
Это поле используется в цепочкеFunctionE
, созданных для каррирования.int parameterCount
- содержит значениеExecutable::getParameterCount()
.
Это поле используется в цепочкеFunctionE
, созданных для каррирования.T self
- содержит вызывающего.null
для статических методов и конструкторов и сам экземпляр для методов экземпляра.int invocationCount
- количество вызовов каррированного экземпляра. В сочетании сparameterCount
помогает определить стадию каррирования.Object[] env
- содержит все параметры.
Метод apply
реализует собственно каррирование. Будем следить за кодом.
26 private static final class FunctionE<T, R> implements Function<T, R> { 27 private static final Object[] EMPTY = new Object[0]; 28 29 private final Executable executable; 30 private final int parameterCount; 31 private final T self; 32 private final int invocationCount; 33 private final Object[] env; 34 35 public FunctionE(Method method) { 36 this(method, null, 0, EMPTY); 37 } 38 39 public FunctionE(Constructor<? extends T> constructor) { 40 this(constructor, null, 0, EMPTY); 41 } 42 43 private FunctionE( 44 Executable executable, 45 T self, 46 int invocationCount, 47 Object[] env) { 48 this.executable = executable; 49 this.parameterCount = executable.getParameterCount(); 50 this.invocationCount = invocationCount; 51 this.self = self; 52 this.env = env; 53 } 54 55 @SuppressWarnings("unchecked") 56 @Override 57 public R apply(T t) { 58 final T newSelf; 59 final Object[] newEnv; 60 61 if (invocationCount == 0) { // initial stage 62 newSelf = t; 63 newEnv = env; 64 } else { // intermediate stage 65 newSelf = self; 66 newEnv = Arrays.copyOf(env, invocationCount); 67 68 newEnv[invocationCount - 1] = t; 69 } 70 71 if (invocationCount == parameterCount) { 72 return invoke(newSelf, newEnv); // final stage 73 } 74 75 return (R) new FunctionE<>( 76 executable, 77 newSelf, 78 1 + invocationCount, 79 newEnv); 80 }
apply
прост: на основе invocationCount
он определяет стадию вызова и действует соответственно. Строки 61–63 представляют начальную стадию, строки 64–68 и 75–79 - промежуточную. Строки 71–73 представляют заключительный этап, на котором вызываетсяexecutable
. Код вызова показан ниже. Обратите внимание, что в строке 66 мы создаем копию env
. Это гарантирует, что мы ничего не изменяем между экземплярами, что гарантирует безопасность потоков.
Метод вызова показан ниже. По причинам, описанным выше (см. • Execution execution
), мы выполняем instanceof
проверку, приведение⁷ и вызов.
... continued from above ... ... 82 @SuppressWarnings("unchecked") 83 private final R invoke(T self, Object[] args) { 84 try { 85 if (executable instanceof Method) { 86 Method m = (Method) executable; 87 return (R) m.invoke(self, args); 88 } else if (executable instanceof Constructor) { 89 Constructor<R> c = (Constructor<R>) executable; 90 return c.newInstance(args); 91 } else { 92 throw new IllegalStateException( 93 "Cannot handle type " + executable.getClass()); 94 } 95 } catch (IllegalAccessException e) { 96 throw new RuntimeException(e); 97 } catch (InstantiationException | InvocationTargetException e) { 98 throw new RuntimeException(e.getCause()); 99 } 100 } 101 }
Можем ли мы сделать больше? Я уверен, что смогли бы. Но это все, что я делаю :-)
Сноски
¹ Весь приведенный выше код доступен в виде общедоступного содержания по адресу https://gist.github.com/lvijay/f3c95f1944b895df1b9f30c682b95b2c. Я также встроил его ниже.
² Страница Википедии на DWIM: https://en.wikipedia.org/wiki/DWIM. Содержит почетное упоминание GNU Emacs.
³ Документы javadoc доступны по адресу https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/lang/reflect/Method.html#invoke(java.lang.Object , java.lang.Object ...) .
⁴ Технически m.invoke(obj, arg1, arg2, ..., argN)
также допустим, но чтобы быть как можно более общим, вы должны поместить все аргументы в Object[]
и передать его invoke
.
⁵ См. §4.11. Ограничения виртуальной машины Java на странице https://docs.oracle.com/javase/specs/jvms/se11/html/jvms-4.html#jvms-4.11.
⁶ Для поклонников шаблонов проектирования это шаблон цепочки ответственности.
⁷ Надеюсь, будущие версии Java упростят церемонию instanceof / cast. См. Jep305 (http://openjdk.java.net/jeps/305) и https://cr.openjdk.java.net/~briangoetz/amber/pattern-match.html.