Лямбда выражения и замыкания

Обновлено: 05.07.2024

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

Чтобы это облегчить, Kotlin, как статически типизированный язык программирования, использует семейство функциональных типов для представления функций и предоставляет набор специализированных языковых конструкций, таких как лямбда-выражения.

Функции высшего порядка

Функция высшего порядка - это функция, которая принимает функции как параметры, или возвращает функцию в качестве результата.

Хорошим примером такой функции является идиома функционального программирования fold для коллекций, которая принимает начальное значение - accumulator вместе с комбинирующей функцией и строит возвращаемое значение, последовательно комбинируя текущее значение accumulator с каждым элементом коллекции, заменяя значение accumulator .

В приведённом выше коде параметр combine имеет функциональный тип (R, T) -> R , поэтому он принимает функцию, которая принимает два аргумента типа R и T и возвращает значение типа R . Он вызывается внутри цикла for и присваивает accumulator возвращаемое значение.

Чтобы вызвать fold , вы должны передать ему экземпляр функционального типа в качестве аргумента и лямбда-выражение (описание ниже). Лямбда-выражения часто используются в качестве параметра функции высшего порядка.

Функциональные типы

Kotlin использует семейство функциональных типов, таких как (Int) -> String , для объявлений, которые являются частью функций: val onClick: () -> Unit = . .

Эти типы имеют специальные обозначения, которые соответствуют сигнатурам функций, то есть их параметрам и возвращаемым значениям:

  • У всех функциональных типов есть список с типами параметров, заключенный в скобки, и возвращаемый тип: (A, B) -> C обозначает тип, который предоставляет функции два принятых аргумента типа A и B , а также возвращает значение типа C . Список с типами параметров может быть пустым, как, например, в () -> A . Возвращаемый тип Unit не может быть опущен;
  • У функциональных типов может быть дополнительный тип - получатель (ориг.: receiver), который указывается в объявлении перед точкой: тип A.(B) -> C описывает функции, которые могут быть вызваны для объекта-получателя A с параметром B и возвращаемым значением C . Литералы функций с объектом-приёмником часто используются вместе с этими типами;
    (ориг.: suspending functions) принадлежат к особому виду функциональных типов, у которых в объявлении присутствует модификатор suspend , например, suspend () -> Unit или suspend A.(B) -> C .

Объявление функционального типа также может включать именованные параметры: (x: Int, y: Int) -> Point . Именованные параметры могут быть использованы для описания смысла каждого из параметров.

Чтобы указать, что функциональный тип может быть nullable, используйте круглые скобки: ((Int, Int) -> Int)? .

При помощи круглых скобок функциональные типы можно объединять: (Int) -> ((Int) -> Unit) .

The arrow notation is right-associative, `(Int) -> (Int) -> Unit` is equivalent to the previous example, but not to `((Int) -> (Int)) -> Unit`. -->

Стрелка в объявлении является правоассоциативной (ориг.: right-associative), т.е. объявление (Int) -> (Int) -> Unit эквивалентно объявлению из предыдущего примера, а не ((Int) -> (Int)) -> Unit .

Вы также можете присвоить функциональному типу альтернативное имя, используя псевдонимы типов.

Создание функционального типа

Существует несколько способов получить экземпляр функционального типа:

Используя блок с кодом внутри функционального литерала в одной из форм:

Литералы функций с объектом-приёмником могут использоваться как значения функциональных типов с получателем.

Используя вызываемую ссылку на существующее объявление:

  • функции верхнего уровня, локальной функции, функции-члена или функции-расширения: ::isOdd , String::toInt ,
  • свойства верхнего уровня, члена или свойства-расширения: List ::size , : ::Regex

К ним относятся привязанные вызываемые ссылки, которые указывают на член конкретного экземпляра: foo::toString .

  • Используя экземпляр пользовательского класса, который реализует функциональный тип в качестве интерфейса:

При достаточной информации компилятор может самостоятельно вывести функциональный тип для переменной.

C` can be passed or assigned where a value of type `A.(B) -> C` is expected, and the other way around: -->

Небуквальные (ориг.: non-literal) значения функциональных типов с и без получателя являются взаимозаменяемыми, поэтому получатель может заменить первый параметр, и наоборот. Например, значение типа (A, B) -> C может быть передано или назначено там, где ожидается A.(B) -> C , и наоборот.

A function type with no receiver is inferred by default, even if a variable is initialized with a reference > to an extension function. > To alter that, specify the variable type explicitly. -->

Обратите внимание, что функциональный тип без получателя выводится по умолчанию, даже если переменная инициализируется со ссылкой на функцию-расширение. Чтобы это изменить, укажите тип переменной явно.

Вызов экземпляра функционального типа

Значение функционального типа может быть вызвано с помощью оператора invoke(. ) : f.invoke(x) или просто f(x) .

Если значение имеет тип получателя, то объект-приёмник должен быть передан в качестве первого аргумента. Другой способ вызвать значение функционального типа с получателем - это добавить его к объекту-приёмнику, как если бы это была функция-расширение: 1.foo(2) .

Встроенные функции

Иногда выгодно улучшить производительность функций высшего порядка, используя встроенные функции (ориг.: inline functions).

Лямбда-выражения и анонимные функции

Лямбда-выражения и анонимные функции - это "функциональный литерал", то есть необъявленная функция, которая немедленно используется в качестве выражения. Рассмотрим следующий пример:

Функция max является функцией высшего порядка, потому что она принимает функцию в качестве второго аргумента. Этот второй аргумент является выражением, которое в свою очередь есть функция, то есть функциональный литерал. Как функция он эквивалентен объявлению:

Синтаксис лямбда-выражений

Полная синтаксическая форма лямбда-выражений может быть представлена следующим образом:

  • Лямбда-выражение всегда заключено в скобки ;
  • Объявление параметров при таком синтаксисе происходит внутри этих скобок и может включать в себя аннотации типов;
  • Тело функции начинается после знака -> ;
  • Если тип возвращаемого значения не Unit , то в качестве возвращаемого типа принимается последнее (а возможно и единственное) выражение внутри тела лямбды.

Если вы вынесите все необязательные объявления, то, что останется, будет выглядеть следующим образом:

Передача лямбды в качестве последнего параметра

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

Такой синтаксис также известен как trailing lambda.

Когда лямбда-выражение является единственным аргументом функции, круглые скобки могут быть опущены.

it: неявное имя единственного параметра

Очень часто лямбда-выражение имеет только один параметр.

` can be omitted. The parameter will be implicitly declared under the name `it`: -->

Если компилятор способен самостоятельно определить сигнатуру, то объявление параметра можно опустить вместе с -> . Параметр будет неявно объявлен под именем it .

Возвращение значения из лямбда-выражения

Вы можете вернуть значение из лямбды явно, используя оператор return. Либо неявно будет возвращено значение последнего выражения.

Таким образом, два следующих фрагмента равнозначны:

Это соглашение, вместе с передачей лямбда-выражения вне скобок, позволяет писать код в стиле LINQ.

Символ подчеркивания для неиспользуемых переменных

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

Деструктуризация в лямбдах

Деструктуризация в лямбдах описана в Деструктурирующие объявления.

Анонимные функции

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

Объявление анонимной функции выглядит очень похоже на обычное объявление функции, за исключением того, что её имя опущено. Тело такой функции может быть описано и выражением (как показано выше), и блоком.

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

Аналогично и с типом возвращаемого значения: он вычисляется автоматически для функций-выражений или же должен быть явно определён (если не является типом Unit ) для анонимных функций с блоком в качетсве тела.

When passing anonymous functions as parameters, place them inside the parentheses. The shorthand syntax that allows you to leave > the function outside the parentheses works only for lambda expressions. -->

Обратите внимание, что параметры анонимных функций всегда заключены в круглые скобки (. ) . Приём, позволяющий оставлять параметры вне скобок, работает только с лямбда-выражениями.

Одним из отличий лямбда-выражений от анонимных функций является поведение оператора return (non-local returns). Слово return , не имеющее метки ( @ ), всегда возвращается из функции, объявленной ключевым словом fun . Это означает, что return внутри лямбда-выражения возвратит выполнение к функции, включающей в себя это лямбда-выражение. Внутри анонимных функций оператор return , в свою очередь, выйдет, собственно, из анонимной функции.

Замыкания

Лямбда-выражение или анонимная функция (так же, как и локальная функция или анонимные объекты) имеет доступ к своему замыканию, то есть к переменным, объявленным вне этого выражения или функции. Переменные, захваченные в замыкании, могут быть изменены в лямбде.

Литералы функций с объектом-приёмником

C`, can be instantiated with a special form of function literals – function literals with receiver. -->

Функциональные типы с получателем, такие как A.(B) -> C , могут быть вызваны с помощью особой формы – литералов функций с объектом-приёмником.

Как было сказано выше, Kotlin позволяет вызывать экземпляр функционального типа с получателем, предоставляющим объект-приёмник.

Внутри тела литерала объект-приёмник, переданный при вызове функции, становится неявным this , поэтому вы можете получить доступ к членам этого объекта-приёмника без каких-либо дополнительных определителей, а обращение к самому объекту-приёмнику осуществляется с помощью выражения this .

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

Ниже приведён пример литерала с получателем вместе с его типом, где plus вызывается для объекта-приёмника:

Синтаксис анонимной функции позволяет вам явно указать тип приёмника. Это может быть полезно в случае, если вам нужно объявить переменную типа нашей функции для использования в дальнейшем.

Лямбда-выражения могут быть использованы как литералы функций с приёмником, когда тип приёмника может быть выведен из контекста. Один из самых важных примеров их использования это типобезопасные строители (ориг.: type-safe builders).

Замыкание (англ. closure ) в программировании — функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её параметрами. Говоря другим языком, замыкание — функция, которая ссылается на свободные переменные в своём контексте.

Содержание

Пример работы замыканий на Delphi (c 2009 версии):

В версиях начиная с 2009, этот код выведет в консоль строки First и Second. Когда переменной типа reference to *** присваивается совместимая по спецификации анонимная подпрограмма или метод, неявно создаётся и инициализируется экземпляр анонимного класса, с полями для хранения значений, используемых подпрограммой из контекста её объявления, методом выполнения (присвоенной подпрограммой) и счётчиком ссылок.

Пример работы замыканий на Scheme:

Функция Array.ConvertAll преобразует один список/массив в другой, применяя для каждого элемента передаваемую ей в качестве параметра функцию.

Метод Select аналогичен методу Array.ConvertAll за тем исключением, что он принимает и возвращает IEnumerable .

В языке C++ замыкание долгое время не поддерживалось. Однако стандарт языка C++11 вводит лямбда-функции и выражения, ограниченно поддерживающие замыкание:

Некоторые языки, такие как Ruby, позволяют выбирать различные способы замыканий по отношению к оператору возврата return :

И Proc.new , так же как и lambda , в этом примере — это способы создания замыкания, но семантика замыканий различна по отношению к оператору return .

PHP имеет встроенную поддержку замыканий начиная с версии 5.3. Пример замыкания. Локальная переменная $id будет увеличиваться при вызове возвращаемой функцией getAdder вложенной функции:

Для более ранних версий возможно использовать одноименный шаблон проектирования, который реализуется в библиотеке Николаса Нассара. P.S. Однако, до сих пор существует проблема с замыканиями в классах, в частности — для статических методов класса.

Java реализует концепцию замыкания с помощью анонимных классов. Анонимный класс имеет доступ к полям класса, в лексическом контексте которого он определён, а также к переменным с модификатором final в лексическом контексте метода.

Пример с использованием замыканий и каррирования:

Пример простого замыкания:

В JavaScript областью видимости локальных переменных (объявляемых словом var) является тело функции, внутри которой они определены.

Если вы объявляете функцию внутри другой функции, первая получает доступ к переменным и аргументам последней:

При этом, такие переменные продолжают существовать и остаются доступными внутренней функции даже после того, как внешняя функция, в которой они определены, была исполнена.

Рассмотрим пример — функцию, возвращающую количество собственных вызовов:

Только после удаления переменной fn, которая ссылается на возвращенную функцию, переменная numberOfCalls будет удалена сборщиком мусора.

Пример с использованием замыканий на Perl:

Пример с использованием замыканий на Lua:

В Haskell замыкания используются повсеместно в виде частичного применения аргументов к функциям (также известного как каррирование).

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

Пример с использованием замыкания на Smalltalk:

Выполнение метода создает замыкание, при использовании которого будет происходить сравнение произвольного аргумента each и связанного значения aComparator.

Пример реализации замыкания в MATLAB с использованием вложенных функций:

Пример реализации замыкания в MATLAB с использованием анонимных функций:

Пример реализации замыкания в Objective-c с использованием блоков (blocks):

Может кто-нибудь объяснить? Я понимаю основные концепции, стоящие за ними, но часто вижу, что они используются взаимозаменяемо, и я запутываюсь.

И теперь, когда мы здесь, чем они отличаются от обычной функции?

Лямбды - это языковая конструкция (анонимные функции), замыкания - это метод реализации для реализации первоклассных функций (анонимных или нет). К сожалению, это часто путают многие люди.

Ответ SasQ превосходен. ИМХО, этот вопрос был бы более полезным для пользователей SO, если бы он направлял зрителей к этому ответу.

Лямбда просто анонимная функция - функция , определенная без имени. В некоторых языках, таких как Scheme, они эквивалентны именованным функциям. Фактически, определение функции переписывается как внутренняя привязка лямбды к переменной. В других языках, таких как Python, между ними есть некоторые (довольно ненужные) различия, но в остальном они ведут себя так же.

Замыкание любая функция , которая закрывает над на окружающую среду , в которой она была определена. Это означает, что он может обращаться к переменным, отсутствующим в его списке параметров. Примеры:

Это приведет к ошибке, потому func что не закрывается в среде anotherfunc - h не определено. func только закрывается над глобальной средой. Это будет работать:

Поскольку здесь func определено в anotherfunc и в Python 2.3 и более поздних версиях (или некотором подобном числе), когда они почти получили правильные замыкания (мутация по-прежнему не работает), это означает, что оно закрывается над anotherfunc средой и может обращаться к переменным внутри Это. В Python 3.1+, мутация тоже работает при использовании на nonlocal ключевое слово .

Еще один важный момент - func будет продолжать закрывать над anotherfunc средой, даже если она больше не оценивается в anotherfunc . Этот код также будет работать:

Это напечатает 10.

Это, как вы заметили, не имеет ничего общего с лямбдами - это два разных (хотя и связанных) понятия.

Клаудиу, насколько я знаю, питон никогда не получал правильных закрытий. Они исправили проблему изменчивости, пока я не смотрел? Вполне возможно .

@AlexanderOrlov: Это и лямбды, и затворы. В Java раньше были замыкания через анонимные внутренние классы. Теперь эта функциональность стала проще синтаксически с помощью лямбда-выражений. Так что, вероятно, наиболее важным аспектом новой функции является то, что теперь есть лямбды. Это не правильно называть их лямбдами, они действительно лямбды. Почему авторы Java 8 могут не выделять тот факт, что они являются замыканиями, я не знаю.

@Claudiu Я думаю, что ссылка на конкретную языковую реализацию (Python) может усложнить ответ. Вопрос полностью не зависит от языка (а также не имеет языковых тегов).

Существует много путаницы вокруг лямбд и закрытий, даже в ответах на этот вопрос StackOverflow здесь. Вместо того, чтобы расспрашивать случайных программистов, которые узнали о замыканиях на практике с определенными языками программирования или другими невежественными программистами, отправляйтесь в путь к источнику (где все это началось). А так как лямбды и закрытие происходят из Lambda Исчисление изобретен Алонзо Черч еще в 30 - е годы , прежде чем первые электронные вычислительные машины существовали даже, это источник я говорю.

Лямбда-исчисление является самым простым языком программирования в мире. Единственное, что в нем можно сделать: ►

Итак, мы решили одну из загадок:
лямбда - это анонимная функция из приведенного выше примера λx.x+2 .

В разных языках программирования синтаксис функциональной абстракции (лямбда) может отличаться. Например, в JavaScript это выглядит так:

и вы можете сразу применить его к какому-либо параметру:

или вы можете сохранить эту анонимную функцию (лямбда) в некоторой переменной:

который фактически дает ему имя f , позволяя вам ссылаться на него и вызывать его несколько раз позже, например:

Но вы не должны были назвать это. Вы можете позвонить сразу:

В LISP лямбды сделаны так:

и вы можете вызвать такую ​​лямбду, применив ее немедленно к параметру:

Хорошо, теперь пришло время разгадать другую загадку: что такое закрытие . Чтобы сделать это, давайте поговорим о символах ( переменных ) в лямбда-выражениях.

Как я уже сказал, лямбда-абстракция выполняет привязку символа в его подвыражении, чтобы он стал заменяемым параметром . Такой символ называется связанным . Но что, если в выражении есть другие символы? Например: λx.x/y+2 . В этом выражении символ x связан лямбда-абстракцией, λx. предшествующей ему. Но другой символ y не связан - он свободен . Мы не знаем, что это и откуда это происходит, поэтому мы не знаем, что это значит и какую ценность оно представляет, и поэтому мы не можем оценить это выражение, пока не выясним, что это y значит.

На самом деле, то же самое относится и к двум другим символам, 2 и + . Просто мы настолько знакомы с этими двумя символами, что обычно забываем, что компьютер их не знает, и нам нужно сказать, что они означают, определив их где-то, например, в библиотеке или в самом языке.

Это позволяет нам разделить лямбда-выражения на две категории:

  • ЗАКРЫТЫЕ выражения: каждый символ, встречающийся в этих выражениях, ограничен какой-то лямбда-абстракцией. Другими словами, они автономны ; они не требуют какого-либо окружающего контекста для оценки. Их также называют комбинаторами .
  • Выражения OPEN: некоторые символы в этих выражениях не являются связанными, то есть некоторые символы, встречающиеся в них, являются свободными и требуют некоторой внешней информации, и поэтому они не могут быть оценены, пока вы не предоставите определения этих символов.

Вы можете ЗАКРЫТЬ открытое лямбда-выражение, предоставив среду , которая определяет все эти свободные символы, связывая их с некоторыми значениями (которые могут быть числами, строками, анонимными функциями, т. Е. Лямбдами, что угодно…).

Например, если у вас есть следующее лямбда-выражение: λx.x/y+2 символ x связан, а символ y свободен, поэтому выражение можно open и не может быть оценено, если вы не скажете, что y означает (и то же самое с + и 2 , которые также свободны). Но предположим, что у вас также есть такая среда :

и это как раз закрытие нашего лямбда-выражения:>

Другими словами, он закрывает открытое лямбда-выражение. Вот откуда и произошло закрытие имени, и поэтому ответы многих людей в этой теме не совсем верны: P

Так почему они ошибаются? Почему многие из них говорят, что замыкания - это некоторые структуры данных в памяти или некоторые особенности языков, которые они используют, или почему они путают замыкания с лямбдами? :П

И это еще хуже из-за того факта, что в их словах всегда есть доля правды, которая не позволяет вам легко отклонить это как ложное: P Позвольте мне объяснить:

Людям не потребовалось слишком много времени, чтобы начать называть фактическую структуру данных, которую они используют в реализациях своего языка, чтобы реализовать замыкание как само замыкание. Структура обычно выглядит примерно так:

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

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

Я надеюсь, что это отвечает на ваши вопросы. Но если у вас есть дополнительные вопросы, не стесняйтесь задавать их в комментариях, и я постараюсь объяснить их лучше.

Замыкание (англ. closure ) в программировании — процедура, которая ссылается на свободные переменные в своём лексическом контексте.

Замыкание, так же как и экземпляр объекта, есть способ представления функциональности и данных, связанных и упакованных вместе.

Замыкание — это особый вид функции. Она определена в теле другой функции и создаётся каждый раз во время её выполнения. В записи это выглядит как функция, находящаяся целиком в теле другой функции. При этом вложенная внутренняя функция содержит ссылки на локальные переменные внешней функции. Каждый раз при выполнении внешней функции происходит создание нового экземпляра внутренней функции, с новыми ссылками на переменные внешней функции.

Замыкание связывает код функции с её лексическим окружением (местом, в котором она определена в коде). Лексические переменные замыкания отличаются от глобальных переменных тем, что они не занимают глобальное пространство имён. От переменных в объектах они отличаются тем, что привязаны к функциям, а не объектам.

Содержание

Реализации замыкания в языках программирования

Scheme

Пример работы замыканий на Scheme:

Nemerle

Nemerle полностью поддерживает замыкания, так как функциональный подход для этого языка родной. Nemerle поддерживает замыкания для локальных функций:

def x = 2; def function(y) < x * y >

Метод Select аналогичен методам Map и Array.ConvertAll за тем исключением, что он принимает и возвращает IEnumerable .

Некоторые языки, такие как Ruby, позволяют выбирать различные способы замыканий по отношению к оператору возврата return . Вот пример на Ruby:

И Proc.new так же как и -> в этом примере, это способы создания замыкания, но семантика замыканий различна по отношению к оператору return .

PHP имеет встроенную поддержку замыканий начиная с версии 5.3. Пример замыкания с одной параметрической и одной контекстной переменной:

Для более ранних версий возможно использовать одноименный шаблон проектирования, который реализуется в библиотеке Николаса Нассара.

Java релизует концепцию замыкания с помощью анонимных классов. Анонимный класс имеет доступ к полям класса, в лексическом контексте которого он определён, а так же к переменным с модификатором final в лексическом контексте метода.

Читайте также: