Ява Карри

Демонстрация каррирования на языке программирования 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 покажется знакомым. Фактически, тело метода в currys -> (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.