Развернутый ответ на вопрос, вынесенный в заглавие поста, приводится в статье Брюса Эккеля в редакции от 25 ноября 2015 года. Мы решили разместить здесь перевод этой статьи и поинтересоваться, что вы думаете о функциональном программировании в Java, а также об актуальности такой книги:



Приятного чтения!



Если кратко – конечно да.

Чтобы дать более развернутый ответ, давайте все-таки разберемся: а зачем мы с ними работаем?

Абстракция поведения

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

// InternalVsExternalIteration.java
import java.util.*;

interface Pet {
    void speak();
}

class Rat implements Pet {
    public void speak() { System.out.println("Squeak!"); }
}

class Frog implements Pet {
    public void speak() { System.out.println("Ribbit!"); }
}

public class InternalVsExternalIteration {
    public static void main(String[] args) {
        List<Pet> pets = Arrays.asList(new Rat(), new Frog());
        for(Pet p : pets) // External iteration
            p.speak();
        pets.forEach(Pet::speak); // Internal iteration
    }
}


Внешняя итерация выполняется в цикле for, причем этот цикл в точности указывает, как она делается. Такой код избыточен и неоднократно воспроизводится в программах. Однако с циклом forEach мы приказываем программе вызвать speak (здесь – при помощи ссылки на метод, которая более лаконична, чем лямбда) для каждого элемента, но нам не приходится описывать, как работает цикл. Итерация обрабатывается внутрисистемно, на уровне цикла forEach.

Такая мотивация “что, а не как” в случае лямбда-выражений является основной. Но, чтобы понять замыкания, нужно подробнее рассмотреть мотивацию функционального программирования как такового.

Функциональное программирование

Лямбда-вырражения/замыкания призваны упростить функциональное программирование. Java 8 – конечно, не функциональный язык, но в нем (как и в Python) теперь обеспечивается некоторая поддержка функционального программирования, эти возможности надстроены над базисной объектно-ориентированной парадигмой.
Основная идея функционального программирования заключается в том, что можно создавать функции и манипулировать ими, в частности, создавать функции во время исполнения. Соответственно, ваша программа может оперировать не только данными, но и функциями. Представьте, какие возможности открываются перед программистом.

В чисто функциональном языке программирования есть и другие ограничения, в частности — инвариантность данных. То есть, у вас нет переменных, только неизменяемые значения. На первый взгляд это ограничение кажется чрезмерным (как вообще работать без переменных?), но оказывается, что, в сущности, при помощи значений достижимо все то же самое, что и с переменными (хотите убедиться – попробуйте Scala, этот язык не является чисто функциональным, но предусматривает возможность везде пользоваться значениями). Инвариантные функции принимают аргументы и выдают результат, не изменяя окружения; поэтому ими значительно проще пользоваться при параллельном программировании, ведь инвариантная функция не блокирует разделяемые ресурсы.
До выхода Java 8 можно было создавать функции во время выполнения только одним способом: генерировать и загружать байт-код (это довольно запутанная и сложная работа).

Для лямбда-выражений характерны две следующие черты:

  1. Более лаконичный синтаксис при создании функций
  2. Возможность создавать функции во время исполнения; затем эти функции могут передаваться другому коду, либо другой код может ими оперировать.


Замыкания касаются именно второй возможности

Что такое замыкание?

При замыкании используются переменные, расположенные вне области действия функции. В традиционном процедурном программировании это не представляет проблемы — вы просто используете переменную — но проблема возникает, как только мы начинаем создавать функции во время исполнения. Чтобы проиллюстрировать эту проблему, сначала приведу пример с Python. Здесь make_fun() создает и возвращает функцию под названиемfunc_to_return, которая затем используется в остальной части программы:

# Closures.py

def make_fun():
    # вне области видимости возвращенной функции:
    n = 0

    def func_to_return(arg):
        nonlocal n
        # Без 'nonlocal' n += arg дает:
        # ссылка на локальную переменную 'n' стоит до присваивания
        print(n, arg, end=": ")
        arg += 1
        n += arg
        return n

    return func_to_return

x = make_fun()
y = make_fun()

for i in range(5):
    print(x(i))

print("=" * 10)

for i in range(10, 15):
    print(y(i))

""" Вывод:
0 0: 1
1 1: 3
3 2: 6
6 3: 10
10 4: 15
==========
0 10: 11
11 11: 23
23 12: 36
36 13: 50
50 14: 65
"""


Обратите внимание, что func_to_return работает с двумя полями, не входящими в ее область видимости: n и arg (в зависимости от конкретного случая, arg может быть копией либо ссылаться на что-либо вне области видимости). Объявление nonlocal является обязательным в силу самого устройства Python: если вы только начинаете работать с переменной, то предполагается, что эта переменная локальна. Здесь компилятор (да, в Python есть компилятор и да, он выполняет кое-какую – признаться, весьма ограниченную – проверку статических типов) видит, что n += arg использует n, которое не было инициализировано в области видимости func_to_return, поэтому генерируется сообщение об ошибке. Но если мы скажем, что n является nonlocal, Python догадается, что мы используем n, определенное вне области видимости функции, и которое было инициализировано, так что все в порядке.

Итак, мы сталкиваемся с такой проблемой: если просто вернуть func_to_return, что будет с n, находящимся вне области видимости func_to_return? Как правило, следовало бы ожидать, что n выйдет из области видимости и станет недоступным, но если это произойдет, то func_to_return не будет работать. Для поддержки динамического создания функций func_to_return должна “замкнуться” вокруг n и обеспечить, чтобы оно «дожило» до возврата функции. Отсюда и термин «замыкание».

Чтобы протестировать make_fun(), мы дважды его вызываем и сохраняем результат функции в x и y. Тот факт, что x и y дают совершенно разные результаты, демонстрирует, что что при каждом вызове make_fun() возникает совершенно самостоятельная функция func_to_return с собственным замкнутым хранилищем для n.

Лямбда-выражения в Java 8

Рассмотрим тот же пример на Java с использованием лямбда-выражений:

// AreLambdasClosures.java
import java.util.function.*;

public class AreLambdasClosures {
    public Function<Integer, Integer> make_fun() {
        // вне области видимости возвращенной функции:
        int n = 0;
        return arg -> {
            System.out.print(n + " " + arg + ": ");
            arg += 1;
            // n += arg; // выдает сообщение об ошибке
            return n + arg;
        };
    }
    public void try_it() {
        Function<Integer, Integer>
            x = make_fun(),
            y = make_fun();
        for(int i = 0; i < 5; i++)
            System.out.println(x.apply(i));
        for(int i = 10; i < 15; i++)
            System.out.println(y.apply(i));
    }
    public static void main(String[] args) {
        new AreLambdasClosures().try_it();
    }
}
/* Output:
0 0: 1
0 1: 2
0 2: 3
0 3: 4
0 4: 5
0 10: 11
0 11: 12
0 12: 13
0 13: 14
0 14: 15
*/


Неоднозначная штука: мы действительно можем обратиться к n, но как только попытаемся изменить n, начнутся проблемы. Сообщение об ошибке таково: local variables referenced from a lambda expression must be final or effectively final (локальные переменные, на которые ставится ссылка из лямбда-выражения, должны быть финальными или фактически финальными).

Оказывается, что лямбда-выражения в Java замыкаются только вокруг значений, но не вокруг переменных. Java требует, чтобы эти значения были неизменны, как если бы мы объявили их final. Итак, они должны быть final независимо от того, объявляли вы их таким образом или нет. То есть, «фактически финальными». Поэтому в Java есть «замыкания с ограничениями», а не «полноценные» замыкания, которые, тем не менее, довольно полезны.

Если мы создаем объекты, расположенные не в куче, то можем изменять такие объекты, поскольку компилятор следит лишь за тем, чтобы не изменялась сама ссылка. Например:

// AreLambdasClosures2.java
import java.util.function.*;

class myInt {
    int i = 0;
}

public class AreLambdasClosures2 {
    public Consumer<Integer> make_fun2() {
        myInt n = new myInt();
        return arg -> n.i += arg;
    }
}



Все компилируется без проблем, чтобы в этом убедиться можете просто поставить ключевое слово final в определение n. Разумеется, если применить такой ход с любой конкуренцией, то возникнет проблема с изменяемым разделяемым состоянием.

Лямбда-выражения – как минимум, отчасти – позволяют достичь желаемой цели: теперь можно создавать функции динамически. Если вы выйдете за границы, то получите сообщение об ошибке, но обычно подобные проблемы решаемы. Выход будет не таким прямолинейным, как на Python, но ведь это все-таки Java. А конечный результат, не лишенный некоторых ограничений (давайте признаем, любой результат в Java не лишен некоторых ограничений) не так уж и плох.

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

Я не усматриваю здесь спора «ООП против ФП», впрочем, и не собирался его устраивать. Более того, я даже «против» здесь не вижу. ООП хорошо подходит для абстрагирования данных (и пусть даже Java вынуждает вас работать с объектами, это еще не означает, что любая задача решаема при помощи объектов), а ФП — для абстрагирования поведений. Обе парадигмы полезны, и, на мой взгляд, тем более полезны, если их смешивать — и в Python, и в Java 8. Недавно мне довелось поработать с Pandoc — конвертером, написанном на чисто функциональном языке Haskell, причем у меня остались от этого самые положительные впечатления. Итак, чисто функциональные языки, также заслуживают места под солнцем.
Актуальность темы

Проголосовало 259 человек. Воздержался 91 человек.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

Комментарии (17)


  1. proos
    06.04.2016 13:23
    +3

    Полагаю, «правильным» определением все же стоит считать то определение, которое появилось первым. Замыкания первыми появились в LISP, и есть отличное определение, которое дал Пол Грем: «When a function refers to a variable defined outside it, it's called a free variable. A function that refers to a free lexical variable is called a closure» Мне кажется, данное определение можно рассматривать как единственно верное по двум причинам: оно основывается на LISP, одном из старейших функциональных языков и такое же определение встречается в JS. Соответственно исходя из такого определения считать лямбды в Java замыканиями ошибочно, но это так же не означает, что лямбды в java реализованы как то хуже — они позволяют делать практически те же вещи, что и замыкания, при учете того, что у final объекта есть внутренне состояние, которое можно изменить с помошью его методов до и после определения самой лямбды.


    1. dougrinch
      06.04.2016 14:05

      Хм, а вот ошибочно ли? Если вы говорите про то, что effectively final variable на самом деле не variable, а value, то разве второе не является просто частным случаем первого?


      1. proos
        06.04.2016 15:55

        Тут вы меня поставили честно говоря в тупик. Здесь есть два важных момента:
        1. free variable — это некая переменная которая не является ни локальной переменной, ни аргументом функции
        2. final variable объявленная до лямбды и использующаяся в лямбде является локальной переменной
        Допустим у нас есть следующий код:

        public class Main {
        
            @FunctionalInterface
            interface FA {
                void doCall();
            }
        
            public static void main(String[] args) {
                final Integer I = new Integer(300);
                FA fa = () -> {
                    System.out.println(I);
                };
            }
        }
        

        Скомпилируем этот класс и посмотрим в байт код лямбды, мы увидим что то такое:
          private static synthetic lambda$main$0(Ljava/lang/Integer;)V
           L0
            LINENUMBER 11 L0
            GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
            ALOAD 0
            INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
           L1
            LINENUMBER 12 L1
            RETURN
           L2
            LOCALVARIABLE I Ljava/lang/Integer; L0 L2 0
            MAXSTACK = 2
            MAXLOCALS = 1
        


        Самая интересная строчка тут: LOCALVARIABLE I Ljava/lang/Integer; L0 L2 0

        Возможно я не прав и ничего не понимаю в bytecode, но тут вроде как локальная переменная, а значит не свободная. И это ведь и логично, в Java мы не можем сделать по другому, не изменив спецификацию JVM, а там, если мне память не изменяет сказано, что для лямбд значение финальной ссылки будет _скопировано_ в тело, и финальные и не финальные пеерменные с точки зрения bytecode вообще не отличимы.


        1. dougrinch
          06.04.2016 16:50
          +1

          Смотря с какой стороны посмотреть. Компилируется оно, действительно, в локальную переменную.

          Но вот с точки зрения языка, как мне кажется, это все-таки свободная переменная. Банально, важным свойством локальной переменной является то, что ее не существует за пределами данного скоупа. Однако код

              public void foo() {
                  int a = 42;
          
                  Runnable r = () -> {
                      System.out.println(a);
                  };
          
                  System.out.println(a);
              }
          

          компилируется. Т.е. область видимости a шире функции.


          1. proos
            06.04.2016 17:46

            Язык это не только синтаксис, язык это его синтаксис и компилятор/интерпритатор(можно бесконечно обсуждать созданные в уме языки программирвоания, но пока у них нет хотя бы одного компилятора — это просто пустая болтовня). И ваш пример совершенно больше демонстрирует то, что здесь имеют место быть 2 переменные с именем a, каждая из которых указывает на один и тот же участок памяти. При создании лямбды ссылка копируется, тем самым a внутри лямбды уже не свободная переменная. И это отличие является причиной необходимости делать final/effectively final внешнюю переменную. Поэтому да, в Java 8 есть лямбды, но нет замыканий. Реализация лямбд близка к реализации замыканий на столько, на сколько это возможно, но вводить энваромент уровня функции или еще какой нибудь способ реализации подобного функционала — на данный моментв JVM не возможен.


            1. dougrinch
              06.04.2016 18:01

              Я бы все-таки различал язык и платформу. В платформе Java замыканий нет (т.к. все копируется и внешних ссылок не остается). А вот языке Java переменная a таки одна.


              1. proos
                06.04.2016 18:50
                +1

                Вот тут член команды BGGA Нил Гафтер поясняет отличия замыкания от анонимного класса(комментарии датированы 2009 годом) и вот что он пишет:

                That binds to a constant, not a variable. The point is to use an identifier in the body of the lambda expression, and have that bound to a lexically enclosing variable. That is one of the defining features of closures, and the facility you describe lacks it.

                Если рассуждать с точки зрения языка, а не с точки зрения компилятора, то любая константа используемая вне своего определения может быть замена своим значением прямо в коде программы.
                В таком случае в Java8 по попросту нет возможности создать замыкание в коде, потому что все свободные переменные будут заменены подстановками, так как они по сути — константы. Тогда, я могу сделать следующий вывод:
                В Java8 замыканий нет, так как не возможно написать такую конструкцию, которая бы была бы замыканием, а не лямбдой.


                1. dougrinch
                  06.04.2016 20:41

                  Любопытная ссылка, спасибо. Но позвольте не согласится. Вы утверждаете что константа не может являться свободной переменной. Однако, в оригинальном лямбда-исчислении (а само понятие «лямбда» вообще-то оттуда и пришло) никаких требований на возможность изменения нет. Более того, там изменяемых переменных и не существует даже. Но это не мешает существованию свободных переменных.


                  1. proos
                    06.04.2016 21:04
                    +1

                    Ну на самом деле есть,

                    The set of free variables in an expression E,
                    denoted by FV(E), is defined by:
                    a) FV( c ) = ? for any constant c
                    b) FV(x) = {x} for any variable x
                    c) FV(E1 E2) = FV(E1) ? FV(E2)
                    d) FV(?x. E) = FV(E) – {x}


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

                    Более того, там изменяемых переменных и не существует даже.

                    Разница между не изменяемой переменной и константой в рамках лямбда исчисления можно представить следующим образом: константа определена вообще в любой момент времени, а вот не изменяеммая переменная только с момента «захвата»(capturing). То есть однажды получив значение, мы можем везде использовать подстановки вида:
                    ?x[x := r]. x = r, до момента «захвата» — нет, не можем.


                    1. dougrinch
                      06.04.2016 22:07

                      Брр. Уже второй раз за последнее время использую слово «константа» в значении «локально неизменяемая». За что, закономерно, и огребаю. Надо отвыкать.

                      Пункт a явно показывает, что множество свободных переменных для любой константы — пустое множество
                      Безусловно. Что «у константы нет свободных переменных» — это очевидно. Я говорил что «сама константа может являться свободной переменной». И это с поправкой, что константа — не литерал «42», а переменная «a = 42», которую мы где-то раньше объявили, но не изменяем.

                      На самом деле, в лямбда-исчисление я, возможно, зря полез, т.к. хоть там и есть термин «свободная переменная», но вот термина «замыкание», на сколько мне известно, нет.

                      Ок, давайте на примере. Рассмотрим терм ?af.fa. Так вот, разве его подтерм ?f.fa не является замыканием? Он ведь содержит свободное вхождение переменной a. Но это в чистом виде джавовая лямбда/анонимный класс:
                          public Function<Function<Integer, Integer>, Integer> foo(Integer a) {
                              return f -> f.apply(a);
                          }
                      


                      почти оффтоп
                      Изначально хотел чтобы типы были попроще и возвращался просто Supplier<Integer>, но не смог написать терм для "() -> a". Ничего кроме просто «a» придумать не получилось, но в чем тогда отличие между «a» и "() -> a". На сколько я помню, абстрагироваться обязательно нужно по какой-то переменной. Т.е. терм "?.a" некорректен.


                      1. Sirikid
                        07.04.2016 11:10

                        ?b.a это же константа, можно даже написать как то так ?_.a


                        1. dougrinch
                          07.04.2016 14:03

                          ?b.a «съедает» один аргумент. Т.е. это (b) -> a. Но вообще да, a и () -> a в чистом лямбда-исчислении явно являются одним и тем же.


        1. olegchir
          06.04.2016 21:16

          Ну можно считать, что effectively final переменная это никакая не переменная, а открытие нового скоупа, записанное таким странным способом (и соответственно, на каждый чих заставляющее плодить мусорные скоупы).

          В качестве примера, можно вместо просто строчки «int a» (которое по вашей логике не free), записать AtomicReference[Integer] a, и теперь a.get() уже free.


          1. proos
            06.04.2016 21:30

            но a.get() — не переменная, все же вызов функции и некая переменная совсем разные вещи.


            1. olegchir
              06.04.2016 22:01

              ну так джава целиком на паттернах, в ней есть многое того, чего в ней нет) Глобальные переменные называются синглтонами, паттерн-матчинги — визитором, замыкания — анонимными внутренними классами, итп :))) То есть на джава-код надо смотреть не на предмет что там написано буквально, а что там происходит на самом деле


  1. Sane
    06.04.2016 18:15
    +4

    Как-то из внешенго мира вопрос в заголовке выглядит как теплое с мягким. Лямбда-функция — это неименованная функция, которую можно объявить в теле метода. Замыкания — возможность использовать перменные, внешние для объявленной функции. Соотвественно, лямбда-функции могут использовать механизм замыканий, а могут и нет — и поддержка замыканий для лямбда функций может быть в языке, а может и нет. Конечно, все эти три вещи идут достаточно близко — лямбда-функции, функции как объект первого порядка (грубо говоря, переменные типа «функции») и замыкания. Язык может не поддерживать лямбда-функции — именно как неименованные функции, но уметь замыкания и наоборот.


  1. mais
    06.04.2016 22:10

    Поделитесь пожалуйста ссылкой на бесплатный оригинал.