В недавней статье ( Что не так в Java 8, часть IV: кортежи ) я сказал, что нет функций нескольких аргументов. Другими словами, функция арности всегда одна. Это, конечно, можно было считать спорным. Я также сказал, что написание функций нескольких аргументов является только синтаксическим сахаром для любого из них:
-
функции одного аргумента кортежа, или
-
функции, возвращающие функции.
Точнее говоря, функция арности n на самом деле либо:
-
функция одного аргумента типа кортеж n или
-
функция одного аргумента, возвращающая функцию арности n — 1
Во втором случае с помощью рекурсии мы можем видеть, что функция арности n может быть преобразована в функцию арности 1 . Его даже можно преобразовать в функцию арности 0 (т. Е. Константу), которая соответствует полному применению функции.
Так в чем же разница между функциями кортежей и функциями, возвращающими функции? Разница связана с тем, когда аргументы оцениваются.
В случае функции кортежа на самом деле есть только один аргумент. Таким образом, то, что может появиться в виде нескольких аргументов, — это значения, которые оцениваются одновременно.
Напротив, если мы переводим то, что кажется функцией арности n, в функцию функции арности n — 1 , мы применяем только один аргумент. Таким образом, этот аргумент оценивается, но остальные могут остаться без оценки. Другими словами:
Integer f(Integer a, Integer b)
может представлять функцию произведения Integer x Integer , которое представляет собой набор всех пар (a, b), где a и b являются целыми числами, к целому числу. Другими словами, тип аргумента f является целым числом x Integer, а тип возвращаемого значения является целым числом
Или он может представлять функцию набора Integer для набора функций от Integer до Integer.
Вот что мы видели в предыдущей статье:
(a, b) -> ...
Может быть заменено на:
a -> b -> ...
Это делается с помощью каррирования (см. Что не так в Java 8, Часть I: Каррирование против замыканий ).
Обратите внимание, что тот факт, что первая функция может быть переписана как вторая, не означает, что они эквивалентны. Другими словами:
f (a, b)
отличается от
(fa) b
хотя они могут дать тот же результат.
Так в чем же разница между ними с точки зрения разработчика? Разница заключается в оценке параметров.
Ява (в основном) строгий язык
Это означает, что в Java вещи обычно оцениваются, как только на них ссылаются. Это подразумевает, что некоторые элементы оцениваются, хотя они могут не использоваться. Например, если мы напишем следующий метод:
Integer compute(Integer a, Integer b) { Integer result = ... // method implementation return result; }
Параметры a и b be будут оцениваться до того, как будет выполнена реализация метода. Может показаться, что это не имеет большого значения, но рассмотрим следующий пример:
public static Integer param() { return 9; } public static Optional<Integer> compute(Integer a, Integer b) { if (b == 0) { return Optional.empty(); } else { return Optional.ofNullable(a / b); } } public static void main(String... args) { compute(param(), 3).ifPresent(System.out::println); }
Этот код напечатает:
3
Теперь замените реализацию на param
:
public static Integer param() { throw new RuntimeException(); }
И измените основной метод на:
compute(param(), 0).ifPresent(System.out::println);
Если мы выполним программу, мы получим:
Exception in thread "main" java.lang.RuntimeException
Хотя код реализации не использует первый параметр (поскольку выполняется только if
ветвь), этот параметр оценивается, поэтому выдается исключение.
В Java нам не предоставляется выбор. Параметры метода всегда оцениваются перед выполнением метода. В некоторых языках, и в частности в функциональных языках, параметры могут оцениваться только при необходимости, что иногда называется «ленивой» оценкой.
Ява иногда ленива?
Как и все языки, Java иногда ленива. На самом деле, было бы гораздо сложнее писать программы на абсолютно строгом языке.
Наиболее известными ленивыми конструкциями в Java являются boolean
операторы &&
и ||
. В отличие от своих двоичных аналогов &
и |
, которые являются строгими, &&
и ||
параметры оцениваются только в случае необходимости. Но в Java их обычно называют не «ленивыми» операторами, а «операторами короткого замыкания».
В результате этой лени и от строгости оценки параметров методы, не представляется возможное , чтобы эмулировать &&
и ||
оператор с методом Java. (Если вы мне не верите, просто попробуйте.) Следующая тривиальная реализация явно нарушена:
public boolean or(boolean a, boolean b) { return a || b; }
потому b
что всегда будет оцениваться, даже если a
есть true
. Таким образом, при оценке b
бросков и исключений этот метод генерирует исключение, даже если a
оценивает true
, хотя a && b
не будет и будет спокойно возвращаться true
.
Есть некоторые другие ленивые конструкции в Java, например
-
троичный оператор
?:
-
if... then...else
-
for
цикл -
while
цикл -
Java 8
Stream
-
Java 8
Optional
Потоки ленивы, и это принципиально. Основной принцип Java-потоков заключается в том, что они не оцениваются до тех пор, пока не будет вызвана терминальная операция. Для получения дополнительной информации см. Предыдущую статью ( Что не так в Java 8, часть III: потоки и параллельные потоки ).
Optional
является ленивым, и он также оценивается только тогда, когда на нем вызывается терминальная операция, хотя «терминальная операция» не относится к словарю Java 8 о Optional
. Но помните из моей предыдущей статьи ( Что не так в Java 8, Часть IV: Монады ), что Optional
, как и Stream
монады. Optional
это как Stream
только из нуля или одного элемента. Вот почему в некоторых функциональных библиотеках или языках Optional
(или эквивалентного типа, такого как Option
или Maybe
) есть forEach
метод вместо Java 8 Optional.ifPresent()
. Инженеры Oracle, вероятно, выбрали, ifPresent
потому что они чувствовали, что forEach
это подходит, только если было более одного элемента.
if...then...else
является ленивой конструкцией, потому что будет оцениваться только одна ветвь в зависимости от условия Еще раз, if...then...else
нельзя эмулировать с помощью метода, такого как:
// T ifThenElse(boolean condition, U if, V else)
потому что все три аргумента (включая if
AND else
) будут оценены перед проверкой условия.
Возможно, это не так очевидно, но циклы Java — это ленивые конструкции. Думать об этом:
for (int i = 0; i < 10; i++) { System.out.println(i); }
Это эквивалентно:
// IntStream.range(0, 10).forEach(System.out::println);
Чтобы было ясно, что цикл является лениво оцененной структурой, давайте перепишем его так:
for (int i = 0;; i++) { if (i < 10) System.out.println(i); else break; }
Это эквивалентно:
// IntStream.range(0, Integer.MAX_VALUE).filter(x -> x < 10).forEach(System.out::println);
Основное отличие состоит в том, что в for
цикле оценка последовательности int
чередуется с эффектом, применимым к каждому int
. С потоками оценка последовательности и эффекта, примененного к каждому элементу, отделена. Оба случая основаны на ленивой оценке. И на самом деле мы можем создавать и обрабатывать бесконечные потоки и бесконечные циклы только потому, что оба они лениво оцениваются. Без ленивой оценки у нас было бы много проблем.
Итак, вопрос: почему мы не можем иметь ленивую оценку везде в Java? Если Java стремится к более функциональному программированию, мы должны иметь возможность выбирать между строгой и нестрогой оценкой параметров метода.
Примечание: вы, возможно, заметили, что IntStream.range(1, Integer.MAX_VALUE)
это не совсем эквивалентно приведенному выше циклу for, потому что for
цикл бесконечный. Тем не менее, возможно построить эквивалент Stream
, но мы должны были бы использовать немного более сложную конструкцию. Но функциональный синтаксис, который мы использовали, вероятно, ближе к тому, что мы предполагали, что конструкция for, индекс которой в конечном итоге переполнится.
Что дальше?
В следующей статье мы сосредоточимся на использовании правильных типов и увидим, что примитивы — это то, что мы не должны использовать. По крайней мере, наши API не должны раскрывать их.
Предыдущие статьи
Что не так в Java 8, часть I: каррирование против замыканий
Что не так в Java 8, часть II: функции и примитивы
Что не так в Java 8, часть III: потоки и параллельные потоки