Статьи

Что не так в Java 8, часть VI: Строгость

В недавней статье ( Что не так в 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)

потому что все три аргумента (включая ifAND 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: потоки и параллельные потоки

Что не так в Java 8, часть IV: монады

Что не так в Java 8, часть IV: кортежи