В чем разница между "замыканием" и "лямбдой"?

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

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

17 ответов

Решение

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

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

def func(): return h
def anotherfunc(h):
   return func()

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

def anotherfunc(h):
    def func(): return h
    return func()

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

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

def anotherfunc(h):
    def func(): return h
    return func

print anotherfunc(10)()

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

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

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

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

  • ПРИМЕНЕНИЕ: Применение одного выражения к другому, обозначается f x,
    (Думайте об этом как вызов функции, где f это функция и x это его единственный параметр)
  • АННОТАЦИЯ: связывает символ, встречающийся в выражении, чтобы отметить, что этот символ - просто "слот", пустое поле, ожидающее заполнения значением, как бы "переменная". Это делается путем добавления греческого письма λ (лямбда), затем символическое имя (например, x), то точка . до выражения. Затем он преобразует выражение в функцию, ожидающую один параметр.
    Например: λx.x+2 принимает выражение x+2 и говорит, что символ x в этом выражении есть связанная переменная - ее можно заменить значением, которое вы указали в качестве параметра.
    Обратите внимание, что функция, определенная таким образом, является анонимной - у нее нет имени, поэтому вы еще не можете обратиться к ней, но вы можете немедленно вызвать ее (помните приложение?), Указав параметр, которого она ожидает, например этот: (λx.x+2) 7, Тогда выражение (в данном случае буквальное значение) 7 заменяется как x в подвыражении x+2 из применяемой лямбда, так что вы получите 7+2, который затем сводится к 9 по общим правилам арифметики.

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


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

function(x) { return x+2; }

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

(function(x) { return x+2; })(7)

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

var f = function(x) { return x+2; }

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

alert(  f(7) + f(10)  );   // should print 21 in the message box

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

alert(  function(x) { return x+2; } (7)  );  // should print 9 in the message box

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

(lambda (x) (+ x 2))

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

(  (lambda (x) (+ x 2))  7  )


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

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

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

Вы можете думать о свободных символах как определенных где-то еще, вне выражения, в его "окружающем контексте", который называется его окружением. Окружение может быть большим выражением, частью которого является это выражение (как сказал Квай-Гон Джинн: "всегда есть большая рыба";)), или в какой-то библиотеке, или в самом языке (как примитив).

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

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

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

И вот идет часть закрытия:
Закрытие лямбда-выражения - это конкретный набор символов, определенных во внешнем контексте (среде), которые присваивают значения свободным символам в этом выражении, делая их несвободными больше. Он превращает открытое лямбда-выражение, которое все еще содержит "неопределенные" свободные символы, в закрытое, в котором больше нет свободных символов.

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

{  y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5  }

Эта среда предоставляет определения для всех "неопределенных" (свободных) символов из нашего лямбда-выражения (y, +, 2) и несколько дополнительных символов (q, w). Символы, которые нам нужно определить, являются этим подмножеством среды:

{  y: 3,
+: [built-in addition],
2: [built-in number]  }

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

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


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

Что ж, виноваты корпоративные маркетоиды Sun/Oracle, Microsoft, Google и т. Д., Потому что так они называли эти конструкции на своих языках (Java, C#, Go и т. Д.). Они часто называют "замыкания", которые должны быть просто лямбдами. Или они называют "замыкания" конкретным методом, который они использовали для реализации лексической области видимости, то есть тот факт, что функция может получить доступ к переменным, которые были определены во внешней области во время ее определения. Они часто говорят, что функция "заключает" эти переменные, то есть записывает их в некоторую структуру данных, чтобы спасти их от уничтожения после завершения выполнения внешней функции. Но это всего лишь выдуманная постфактумная "фольклорная этимология" и маркетинг, которая только делает вещи более запутанными, потому что каждый поставщик языка использует свою собственную терминологию.

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

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

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

Closure {
   [pointer to the lambda function's machine code],
   [pointer to the lambda function's environment]
}

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

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

Все остальное - просто "культ груза" и "магия воу-ду" программистов и поставщиков языков, не подозревающих о реальных корнях этих понятий.

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

Когда большинство людей думают о функциях, они думают о названных функциях:

function foo() { return "This string is returned from the 'foo' function"; }

Они называются по имени, конечно:

foo(); //returns the string above

С лямбда-выражениями вы можете иметь анонимные функции:

 @foo = lambda() {return "This is returned from a function without a name";}

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

foo();

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

function filter(list, predicate) 
 { @filteredList = [];
   for-each (@x in list) if (predicate(x)) filteredList.add(x);
   return filteredList;
 }

//filter for even numbers
filter([0,1,2,3,4,5,6], lambda(x) {return (x mod 2 == 0)}); 

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

@x = 0;

function incrementX() { x = x + 1;}

incrementX(); // x now equals 1

Это не так много, но что, если все это было в другой функции, и вы прошли incrementX к внешней функции?

function foo()
 { @x = 0;

   function incrementX() 
    { x = x + 1;
      return x;
    }

   return incrementX;
 }

@y = foo(); // y = closure of incrementX over foo.x
y(); //returns 1 (y.x == 0 + 1)
y(); //returns 2 (y.x == 1 + 1)

Вот как вы получаете объекты с состоянием в функциональном программировании. Поскольку именование "incrementX" не требуется, вы можете использовать лямбду в этом случае:

function foo()
 { @x = 0;

   return lambda() 
           { x = x + 1;
             return x;
           };
 }

Не все замыкания являются лямбдами, и не все лямбды являются замыканиями. Оба являются функциями, но не обязательно так, как мы привыкли знать.

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

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

В объектно-ориентированном языке замыкания обычно предоставляются через объекты. Однако некоторые языки OO (например, C#) реализуют специальные функциональные возможности, которые ближе к определению замыканий, предоставляемых чисто функциональными языками (такими как lisp), которые не имеют объектов для включения состояния.

Интересно то, что введение Lambdas и Closures в C# приближает функциональное программирование к общепринятому использованию.

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

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

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

С точки зрения языков программирования, это абсолютно разные вещи.

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

Лямбда предоставляет способ абстрагировать вычислительный процесс. например, чтобы вычислить сумму двух чисел, процесс, который принимает два параметра x, y и возвращает x+y, может быть абстрагирован. В схеме вы можете написать это как

(lambda (x y) (+ x y))

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

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

((lambda (x y) (+ x y)) 2 3)

Мы можем просто заменить параметры выражением для оценки. Эта модель уже очень мощная. Но эта модель не позволяет нам изменять значения символов, например, мы не можем имитировать изменение статуса. Таким образом, нам нужна более сложная модель. Короче говоря, всякий раз, когда мы хотим вычислить значение лямбда-выражения, мы помещаем пару символов и соответствующее значение в среду (или таблицу). Затем остаток (+ x y) оценивается путем поиска соответствующих символов в таблице. Теперь, если мы предоставим некоторые примитивы для непосредственной работы с окружающей средой, мы можем смоделировать изменения статуса!

С этим фоном, проверьте эту функцию:

(lambda (x y) (+ x y z))

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

((lambda (z) (lambda (x y) (+ x y z))) 1)

Таким образом, z будет привязан к 1 во внешней таблице. Мы все еще получаем функцию, которая принимает два параметра, но реальное значение этого также зависит от внешней среды. Другими словами, внешняя среда замыкается на свободные переменные. С помощью set!, мы можем сделать функцию состоящей из состояний, то есть это не функция в смысле математики. То, что он возвращает, зависит не только от ввода, но и от z.

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

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

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

Идея та же, что описана выше, но если вы из PHP, это более подробно объясняется с использованием PHP-кода.

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, function ($v) { return $v > 2; });

function ($ v) {return $ v> 2; } - это определение лямбда-функции. Мы даже можем сохранить его в переменной, чтобы его можно было использовать повторно:

$max = function ($v) { return $v > 2; };

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max);

А что если вы хотите изменить максимально допустимое число в фильтруемом массиве? Вам придется написать еще одну лямбда-функцию или создать замыкание (PHP 5.3):

$max_comp = function ($max) {
  return function ($v) use ($max) { return $v > $max; };
};

$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max_comp(2));

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

Вот более простой пример закрытия PHP:

$string = "Hello World!";
$closure = function() use ($string) { echo $string; };

$closure();

Красиво объясняется в этой статье.

Этот вопрос старый и получил много ответов. Теперь с Java 8 и Official Lambda, которые являются неофициальными проектами закрытия, это поднимает вопрос.

Ответ в контексте Java (через лямбды и замыкания - в чем разница?):

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

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

Lambdaявляется анонимной функцией (метод)

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

Ява

      interface Runnable {
    void run();
}

class MyClass {
    void foo(Runnable r) {

    }

    //Lambda
    void lambdaExample() {
        foo(() -> {});
    }

    //Closure
    String s = "hello";
    void closureExample() {
        foo(() -> { s = "world";});
    }
}

Swift <sup>[Закрытие]</sup>

      class MyClass {
    func foo(r:() -> Void) {}
    
    func lambdaExample() {
        foo(r: {})
    }
    
    var s = "hello"
    func closureExample() {
        foo(r: {s = "world"})
    }
}

Лямбда-выражение - это просто анонимная функция. в простой Java, например, вы можете написать это так:

Function<Person, Job> mapPersonToJob = new Function<Person, Job>() {
    public Job apply(Person person) {
        Job job = new Job(person.getPersonId(), person.getJobDescription());
        return job;
    }
};

где класс Function просто встроен в код Java. Теперь вы можете позвонить mapPersonToJob.apply(person) где-то это использовать. это только один пример. Вот лямбда, прежде чем был синтаксис для этого. Лямбда - короткий путь для этого.

Закрытие:

лямбда становится замыканием, когда она может получить доступ к переменным вне этой области. Я думаю, вы можете сказать, что это волшебство, оно волшебным образом может обернуться вокруг среды, в которой оно было создано, и использовать переменные вне своей области видимости (внешняя область. Поэтому, чтобы быть ясным, закрытие означает, что лямбда может получить доступ к своей ВНЕШНЕЙ ОБЛАСТИ).

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

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

Уточнение терминологии

Лучше знать, что термины «замыкание» и «лямбда» могут обозначать разные вещи в зависимости от контекста.

Это формальный вопрос, потому что спецификация обсуждаемого PL (языка программирования) может явно определять такие термины.

Например, по ISO C++ (начиная с C++11):

Тип лямбда-выражения (который также является типом объекта замыкания) — это уникальный безымянный тип класса без объединения, называемый типом замыкания, свойства которого описаны ниже.

Поскольку пользователи C-подобных языков ежедневно путают «указатели» (типы) с «значениями указателей» или «объектами-указателями» (обитателями типов), здесь тоже есть риск запутаться: большинство пользователей C++ на самом деле говорят о «объекты закрытия», используя термин «закрытие». Будьте осторожны с двусмысленностью.

ПРИМЕЧАНИЕ . Чтобы сделать вещи в целом более ясными и точными, я бы редко преднамеренно использовал некоторые нейтральные к языку термины (обычно относящиеся к теории ЯП вместо терминологии, определяемой языком. Например, использованный выше тип обитатель охватывает специфичные для языка "(r)values» и «lvalues» в более широком смысле. (Поскольку синтаксическая сущность определения категории значений C++ не имеет значения, избегание «(l/r)values» может уменьшить путаницу). (Отказ от ответственности: lvalues ​​и rvalues ​​достаточно распространены в во многих других контекстах.) Термины, формально не определенные среди разных ЯВ, могут быть заключены в кавычки Дословная копия из материалов, на которые даны ссылки, также может быть заключена в кавычки без опечаток.

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

Соответствующие конструкции в современных ЯП обычно называют «лямбда-выражениями». И это происходит от «лямбда-абстракций», обсуждаемых ниже.

Перед подробными обсуждениями рекомендую прочитать некоторые комментарии к самому вопросу. Я чувствую, что они безопаснее и полезнее, чем большинство ответов на вопрос здесь, в смысле меньшего риска запутаться. (К сожалению, это самая важная причина, по которой я решил дать ответ здесь...)

Лямбды: краткая история

Конструкции, называемые «лямбда» в ЯП, независимо от «лямбда-выражения» или чего-то другого, являются синтаксическими . Другими словами, пользователи языков могут найти такие конструкции исходного языка , которые используются для создания чего-то другого. Грубо говоря, «другие» на практике являются просто «анонимными функциями».

Такие конструкции происходят из лямбда-абстракций , одной из трех синтаксических категорий («видов выражений») (нетипизированного) лямбда-исчисления , разработанного А. Чёрчем.

Лямбда-исчисление — это система вывода (точнее, TRS (система перезаписи терминов)) для универсального моделирования вычислений. Уменьшить лямбда-термин так же, как вычислить выражение в обычных ЯП. Со встроенными правилами редукции достаточно определить различные способы вычислений. (Как вы, возможно, знаете, он является полным по Тьюрингу.) Следовательно, его можно использовать как PL.

ПРИМЕЧАНИЕ Вычисление выражения в PL не является взаимозаменяемым с сокращением термина в TRS в целом. Однако лямбда-исчисление — это язык, в котором все результаты редукции могут быть выражены в исходном языке (т. е. в виде лямбда-терминов), поэтому они имеют одинаковое значение по совпадению. Почти все ЯП на практике не обладают этим свойством; исчисление для описания их семантики может содержать термины, не являющиеся выражениями исходного языка, и сокращения могут иметь более подробные эффекты, чем оценки.

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

ПРИМЕЧАНИЕ Лямбда-абстракция может вводить только один параметр. Чтобы преодолеть ограничение внутри исчисления, см . Currying.

Возможность введения переменных делает лямбда-исчисление типичным языком высокого уровня (хотя и простым). С другой стороны, комбинаторную логику можно рассматривать как ЯП, если убрать переменные и абстракции из лямбда-исчисления. Именно в этом смысле комбинаторная логика является низкоуровневой: это как старые добрые языки ассемблера, которые не позволяют вводить переменные, названные пользователем (несмотря на макросы, что требует дополнительной препроцессорной обработки). (... Если не более низкоуровневый... обычно языки ассемблера могут, по крайней мере, вводить метки с именами пользователей.)

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

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

Хотя лямбда-исчисления не предназначены для непосредственного использования в качестве ЯП, реализованных в компьютерах, на практике они повлияли на ЯП. Примечательно, что Дж. Маккарти представил оператор в LISP для предоставления функций, точно соответствующих идее нетипизированного лямбда-исчисления Черча. Судя по всему, название произошло от буквы λ. LISP (позже) имеет другой синтаксис (S-выражение ), но все программируемые элементы в LAMBDAвыражения могут быть напрямую сопоставлены с лямбда-абстракциями в нетипизированном лямбда-исчислении с помощью тривиальных синтаксических преобразований.

С другой стороны, многие другие ЯП выражают аналогичные функциональные возможности другими средствами. Несколько иной способ введения повторно используемых вычислений — это именованные функции (или, точнее, именованные подпрограммы), которые поддерживаются более ранними языками программирования, такими как FORTRAN, и языками, производными от ALGOL. Они представлены синтаксисом, указывающим, что именованный объект является одновременно и функцией. Это в некотором смысле проще по сравнению с диалектами LISP (особенно в аспекте реализации) и кажется более популярным, чем диалекты LISP на протяжении десятилетий. Именованные функции также могут разрешать расширения, не используемые анонимными функциями, например перегрузку функций.

Тем не менее, все больше и больше промышленных программистов, наконец, находят полезность первоклассных функций , и требования к возможности вводить определения функций на месте (в выражениях в произвольных контекстах, скажем, в качестве аргумента какой-либо другой функции) возрастают. . Естественно и законно избегать именования того, что не обязательно должно быть, и любые именованные функции здесь не работают по определению. (Возможно, вы знаете, что правильное наименование вещей — одна из хорошо известных сложных задач в информатике..) Чтобы решить эту проблему, анонимные функции вводятся в языки, традиционно предоставляющие только именованные функции (или подобные функциям конструкции, такие как «методы»), такие как C++ и Java. Многие из них называют эту функцию «лямбда-выражениями» или подобными лямбда-вещами, потому что они в основном отражают одну и ту же идею в лямбда-исчислениях. Эпоха Возрождения.

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

Замыкания: история

Замыкания в математике не то же самое, что и в ЯП.

В последнем контексте этот термин г. для обеспечения поддержки первоклассных функций при реализации оценки ЯП, «смоделированных в λ-нотации Чёрча».

Для модели, предложенной Лэндином ( машина SECD ), замыкание состоит из λ-выражения и среды, относительно которой оно оценивалось , или, точнее:

часть среды, представляющая собой список, двумя элементами которого являются (1) среда (2) идентификатор списка идентификаторов

и часть управления, которая состоит из списка, единственным элементом которого является AE

ПРИМЕЧАНИЕ . Аббревиатура АЕ используется в статье как аппликативное выражение . Это синтаксис, раскрывающий более или менее ту же функциональность приложения в лямбда-исчислении. Есть также некоторые дополнительные детали, такие как «аппликативные» .не так интересно в лямбда-исчислении (потому что оно чисто функциональное). SECD не согласуется с исходным лямбда-исчислением для этих незначительных различий. Например, SECD останавливается на произвольной одиночной лямбда-абстракции независимо от того, имеет ли подтермин («тело») нормальную форму, потому что он не будет сокращать подтермин («оценивать тело») без применения абстракции («вызывается»). Однако такое поведение может быть больше похоже на сегодняшние ЯП, чем на лямбда-исчисление. SECD также не единственная абстрактная машина, которая может вычислять лямбда-члены; хотя большинство других абстрактных машин для аналогичной цели также могут иметь среду. В отличие от лямбда-исчисления (которое является чистым), эти абстрактные машины могут в некоторой степени поддерживать мутацию.

Таким образом, в этом конкретном контексте замыкание представляет собой внутреннюю структуру данных для реализации конкретных оценок PL с AE.

Дисциплина доступа к переменным в замыканиях отражает лексическую область видимости , впервые использованную в начале 1960-х годов в императивном языке ALGOL 60. ALGOL 60 поддерживает вложенные процедуры и передачу процедур параметрам, но не возвращает процедуры в качестве результатов. Поскольку языки имеют полную поддержку функций первого класса, которые могут быть возвращены функциями, статическая цепочка в реализациях в стиле ALGOL 60 не работает, поскольку свободные переменные, используемые возвращаемой функцией, могут больше не присутствовать в стеке вызовов. Это восходящая проблема funarg . Замыкания решают проблему, захватывая свободную переменную в частях среды и избегая их размещения в стеке.

С другой стороны, все ранние реализации LISP используют динамическую область видимости. Это делает все привязки переменных, на которые ссылаются, доступными в глобальном хранилище, а сокрытие имен (если таковые имеются) реализуется для каждой переменной: как только переменная создается с существующим именем, старое имя поддерживается структурой LIFO; другими словами, имя каждой переменной может обращаться к соответствующему глобальному стеку. Это эффективно устраняет необходимость в средах для каждой функции, потому что в функции никогда не захватываются свободные переменные (они уже «захвачены» стеками).

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

ПРИМЕЧАНИЕ Таким образом, среды не являются существенными для сокращения лямбда-членов. Однако исчисление, расширяющее лямбда-исчисление, может явно ввести среды в грамматику, даже если оно моделирует только чистые вычисления (без мутаций). Путем явного добавления сред могут быть специальные правила ограничений для сред, обеспечивающие нормализацию среды, что усиливает эквациональную теорию исчисления. (См. §9.1.)

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

Реализации LISP действительно поддерживают первоклассные функции на самом раннем этапе, но с чистой динамической областью видимости нет реальной проблемы с функциями аргументов: они могут просто избежать выделения памяти в стеке и позволить глобальному владельцу (GC, сборщику мусора) управлять ресурсы в средах (и записи активации), ссылающиеся на переменные. Тогда замыкания не нужны. И это ранние реализации до изобретения замыканий.

Глубокое связывание , которое приближается к статическому (лексическому) связыванию, было введено примерно в 1962 году в LISP 1.5 через FUNARGустройство. Это, наконец, сделало проблему известной под названием «проблема фунарга».

ПРИМЕЧАНИЕ . AIM-199 указывает, что это в основном относится к окружающей среде.

Scheme — это первый диалект Лиспа, поддерживающий лексическую область видимости по умолчанию (динамическая область видимости может быть смоделирована с помощью make-parameter/ parameterizeформы в современных версиях Scheme). В более позднее десятилетие были некоторые дебаты, но, в конце концов, большинство диалектов Лиспа приняли идею по умолчанию использовать лексическую область видимости, как это делают многие другие языки. С тех пор замыкание, как техника реализации, получило более широкое распространение и большую популярность среди ЯП разного вкуса.

Замыкания: эволюция

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

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

Точно так же современные реализации могут не сохранять синтаксические конструкции (AE или лямбда-термины) непосредственно в качестве управляющей части. Вместо этого они могут использовать некоторое внутреннее IR (промежуточное представление) или "скомпилированную" форму (например, FASL, используемый некоторыми реализациями диалектов Lisp). Такой ИР даже не гарантированно генерируется из lambdaформы (например, это может происходить из тела некоторых именованных функций).

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

Пересмотр терминологии, относящейся к PL

Кроме того, некоторые языки могут определять термины, связанные с «замыканием», в своей спецификации, чтобы имена объектов могли быть реализованы замыканиями. Это прискорбно, потому что это приводит ко многим неправильным представлениям, например, «замыкание — это функция». Но, к счастью, большинство языков, по-видимому, избегают прямо называть его синтаксической конструкцией языка.

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

  • «объекты» перенаправляются в «экземпляры классов» (в языках Java/CLR/«ООП») вместо традиционного «типизированного хранилища» (в C и C++) или просто «значения» (во многих Lisps);

  • «переменные» перенаправляются на что-то традиционное, называемое «объектами» (в Golang), а также в изменяемые состояния (во многих новых языках), поэтому это больше не совместимо с математикой и чистыми функциональными языками;

  • «полиморфизм» ограничен полиморфизмом включения (в языках C++/«ООП»), даже эти языки имеют другие виды полиморфизма (параметрический полиморфизм и специальный полиморфизм).

Об управлении ресурсами

Несмотря на то, что в современных реализациях компоненты опущены, определения в статье Ландина довольно гибкие. Это не ограничивает способ хранения компонентов, таких как среды, вне контекста машины SECD.

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

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

  • В C++ пользователям разрешается явно управлять ресурсами сущностей, захваченных в замыканиях, путем указания способа захвата каждой переменной в списке захвата лямбда-выражения (путем копирования значения, ссылки или даже явного инициализатора) и точного типа каждой переменной (умные указатели или другие типы). Это может быть небезопасно, но при правильном использовании становится более гибким.

  • В Rust ресурсы захватываются с помощью разных режимов захвата (путем неизменного заимствования, заимствования, перемещения), которые пробуются по очереди (реализацией), и пользователи могут явно указать move. Это более консервативно, чем C++, но в некотором смысле безопаснее (поскольку заимствования проверяются статически, по сравнению с непроверенными захватами по ссылке в C++).

Все вышеперечисленные стратегии могут поддерживать замыкания (C++ и Rust имеют специфичные для языка определения понятия «тип замыкания»). Дисциплины управления ресурсами, используемыми замыканиями, не имеют ничего общего с квалификацией замыканий.

Итак, (хотя это и не видно здесь) утверждение Томаса Лорда из LtU о необходимости трассировки графа для замыканий также технически неверно. Замыкания могут решить проблему funarg, потому что они позволяют предотвратить недопустимый доступ к записи активации (стеку), но факт не гарантирует, что все операции над ресурсами, включающими замыкание , будут допустимы. Такой механизм зависит от внешней среды выполнения. Должно быть ясно, что даже в традиционных реализациях неявный владелец (GC) не является компонентомзамыкания и наличие владельца - это деталь реализации машины SECD (поэтому это одна из деталей "высокого порядка" для пользователей). Поддерживает ли такая деталь трассировку графа или нет, это не влияет на квалификацию замыканий. Кроме того, насколько я знаю, введен П. Дж. Ландином в 1964языковые конструкции letв сочетании с recвпервые введен (опять же П. Ландином) в ISWIM в 1966 году, что не могло иметь последствий для обеспечения соблюдения первоначального значения замыканий, изобретенных ранее, чем само по себе.

Отношения

Таким образом, чтобы суммировать их, замыкание может быть (неформально) определено как:

(1) структуру данных, специфичную для реализации ЯП, включающую в себя часть среды и часть управления для функционально-подобного объекта, где:

(1.1) часть управления получена из некоторых конструкций исходного языка, определяющих конструкцию оценки функционально-подобного объекта;

(1.2) часть среды состоит из среды и, возможно, других данных, определяемых реализацией;

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

(2) в качестве альтернативы, общий термин метода реализации для использования объектов, названных «замыканиями» в (1).

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

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

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

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

Лямбда-выражения могут оцениваться как функциональные сущности. Пользователи некоторых ЯВ могут называть такой функциональный объект «замыканием». «Анонимная функция» должна быть более нейтральным названием такого «закрытия» в этом контексте.

Приложение: функции: запутанная история

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

Это уже бардак в математике.

В настоящее время я слишком ленив, чтобы суммировать их в контексте ЯП, но в качестве предостережения: следите за контекстом, чтобы убедиться, что различные определения «функции» в разных ЯП не делают ваши рассуждения предвзятыми по теме.

Что касается использования «анонимных функций» в целом (совместно применяемых PL на практике), я полагаю, что это не внесет значительных путаниц и неправильных представлений по этой теме.

Именованные функции могут иметь немного больше проблем. Функции могут обозначать сущность самого имени («символы»), а также оцениваемые значения этих имен. Учитывая, что тот факт, что большинство ЯП не имеют неопределенного контекста, позволяющего отличить функцию от некоторых других сущностей, несущих интересное значение (например, sizeof(a_plain_cxx_function)в C++ просто неправильно сформированы), пользователи могут не заметить различий в неправильном толковании между невычисленным операндом и оцененными значениями. Это будет проблематично для некоторых диалектов Лиспа, имеющих QUOTE. Даже опытные специалисты по языкознанию легко могут упустить что-то важное ; именно поэтому я подчеркиваю необходимость отличать синтаксические конструкции от других сущностей.

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

Внешние переменные - переменные, определенные вне области действия функции.

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

    Function<Integer,Integer> lambda = t -> {
        int n = 2
        return t * n 
    }
    
  • Замыкания содержат состояние, потому что для выполнения операций используются внешние переменные (т. Е. Переменные, определенные вне области действия тела), а также параметры и константы.

    int n = 2
    
    Function<Integer,Integer> closure = t -> {
        return t * n 
    }
    

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

Этому вопросу 12 лет, и мы до сих пор получаем его как первую ссылку в Google для «замыканий против лямбда». Поэтому я должен сказать это так, как никто не сказал прямо.

Лямбда-выражение - это анонимная функция (объявление).

И завершение, цитируя Прагматику языка программирования Скотта, объясняется следующим образом:

… Создание явного представления ссылающейся среды (обычно той, в которой подпрограмма будет выполняться, если она будет вызвана в настоящее время) и объединение его вместе со ссылкой на подпрограмму… называется закрытием.

То есть это то же самое, что мы называем связкой «функция + контекст сдачи».

Лямбда - это определение анонимной функции, которое (не обязательно) связано с идентификатором.

«Анонимные функции берут начало в работе Алонзо Черча, изобретшего лямбда-исчисление, в котором все функции анонимны» - Википедия

Замыкание - это реализация лямбда-функции.

«Питер Дж. Ландин определил термин« закрытие »в 1964 году как имеющий часть среды и часть управления, используемые его машиной SECD для оценки выражений» - Википедия

Общее объяснение Lambda и Closure рассматривается в других ответах.

Для тех, кто знаком с C++, лямбда-выражения были введены в C++ 11. Думайте о Lambdas как о удобном способе создания анонимных функций и функциональных объектов.

«Различие между лямбдой и соответствующим закрытием в точности эквивалентно различию между классом и экземпляром класса. Класс существует только в исходном коде; он не существует во время выполнения. То, что существует во время выполнения, является объектами Тип класса. Замыкания относятся к лямбдам так же, как объекты относятся к классам. Это не должно быть сюрпризом, потому что каждое лямбда-выражение вызывает создание уникального класса (во время компиляции), а также вызывает создание объекта этого типа класса, замыкания. (во время выполнения) ". - Скотт Майерс

C++ позволяет нам исследовать нюансы Lambda и Closure, поскольку вам нужно явно указать свободные переменные, которые нужно захватить.

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

      void register_func(void(*f)(int val))   // Works only with an EMPTY capture list
{
    int val = 3;
    f(val);
}
 
int main() 
{
    int env = 5;
    register_func( [](int val){ /* lambda body can access only val variable*/ } );
}

Как только в список захвата добавляется свободная переменная из окружающей среды ( [env]), должно быть сгенерировано замыкание.

          register_func( [env](int val){ /* lambda body can access val and env variables*/ } );

Поскольку это уже не обычная функция, а закрытие, возникает ошибка компиляции.
no suitable conversion function from "lambda []void (int val)->void" to "void (*)(int val)" exists

Ошибка может быть исправлена ​​с помощью обертки функции std::function который принимает любую вызываемую цель, включая сгенерированное закрытие.

      void register_func(std::function<void(int val)> f)

См. Lambda и Closure для подробного объяснения на примере C++.

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

Закрытие означает, что функция возвращает другую функцию. Не результат, а вызываемая функция, как это делают делегаты. Lambda — это анонимное описание функции. Лямбда также может быть замыканием, если она возвращает функцию.

Другие вопросы по тегам