Анонимные и лямбда функции

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

Анонимная функция

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

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

[-1, 0, -3, 2, 3].filter(n => n > 0); // [2, 3]

Функция-стрелка в JS всегда анонимна, ее как раз для этого и придумали.

Присваивание анонимной функции часто встречается в обработке событий:

window.onload = function() {
alert('Page has been loaded.');
}

Вызов функции на месте в JS обычно используют для ограничения области видимости:

(function () {
// Переменные внутри функции не видны сраружи
const inner = "You can't see me from outside!";
})();

console.info(inner); // inner is not defined

С появлением модулей и классов в JS этот прием уже не так актуален.

Лямбда-функция

Современные языки программирования часто заимствуют приемы работы с функциями из лямбда-исчисления — математического аппарата, который в 1930-е разработал Алонзо Черч
Алонзо Чёрч, американский математик.
для того, чтобы разобраться с понятием вычислимости.

Определение

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

Запись λx.M означает, что функция принимает аргумент x (пишется после лямбды, перед точкой) и использует его в выражении M. Эту же запись можно представить так: x → M.

Например, в выражении λx.x*3 аргументом является x, который в теле функции умножается на три. Это же выражение можно записать так:

Привет функции-стрелки из ES2015

x => x * 3

Почему лямбда?

Символ особого значения не имеет, но всегда интересно узнать подноготную.

Черчу нужно было как-то обозначить абстрактную сущность, которая принимает аргумент. До Черча похожие исследования уже проводились и в них использовался символ . Исследования Черча отличались и он, чтоб не путать понятия, стал писать крышку сбоку, а не сверху: ∧x. Потом, для удобства печати, клин заменили на λ.

By the way, why did Church choose the notation λ? In [Church, 1964, §2] he stated clearly that it came from the notation used for class-abstraction by Whitehead and Russell, by first modifying to ∧x to distinguish function-abstraction from class-abstraction, and then changing to λ for ease of printing. This origin was also reported in [Rosser, 1984, p.338 ]. On the other hand, in his later years Church told two enquirers that the choice was more accidental: a symbol was needed and λ just happened to be chosen.

…Church was not the first to introduce an explicit notation for function-abstraction. But he was the first to state explicit formal conversion rules for the notation, and to analyse their consequences in depth.

Связанные и свободные переменные

Переменная, которая передается в функцию и используется в ее теле называется связанной. x в примере выше — связанная переменная. А, например, z в выражении λx.x+zсвободная, потому что берется откуда-то из внешней среды (привет замыкания).

Основная задача лямбда-выражения вида λx.M — обозначать связывание переменных.

Характеристики λ-функции

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

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

Применение в программировании

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

Алонзо Чёрч, 1932

На основе лямбда-исчисления придумали функциональные языки: Lisp, Haskell, Miranda, ML и пр. Программа на таком языке — это выражение, составленное из других выражений, вычисляя которое мы решаем свою задачу (реализуем алгоритм). Как 2+3 возвращает 5, так и программа на функциональном языке должна в итоге что-то вернуть.

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

Разные языки программирования решают разные задачи. Например, Хаскель и Миранда придуманы для математиков, поэтому они должны как можно четче соответствовать принципам лямбда-исчисления. А вот Питону вообще пофиг на лямбда-исчисление, потому что это язык для обычных людей, которым надо решать свои повседневные задачи. То же самое касается и JS.

Языки общего назначения не пытаются соответствовать абстрактному математическому аппарату, они просто заимствуют из него некоторые возможности.

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

Лисп:

((lambda (msg) (print msg)) "hello")

Питон:

(lambda msg: print(msg))("hello")

JavaScript:

((msg) => console.log(msg))("hello");

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

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

Анонимная функция vs лямбда-функция

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

После всего вышеописанного та заметка, с которой всё началось, уже не кажется такой интересной. Все зависит от языка. Хотим — называем анонимные функции лямбдами, не хотим — не называем. Но можно принять некоторые допущения и продолжить :)

Автор говорит, что Lambda means function used as data. Примем это утверждение. Будем считать, что в JS функция, которая приходит аргументом, возвращается из функции или присваивается в переменную является лямбдой. Про сайд-эффекты и количество параметров не паримся.

Противоречие первое: функция в IIFEImmediately Invoked Function Expression. не рассматривается как данные.
Автор говорит, что вызываемая на месте функция (IIFE) — не данные и поэтому не лямбда. Мы, мол, эту функцию сразу же выполняем и забываем о ней.

Все так, IIFE — не данные и не лямбда… и не функция. Это выражение, в которое мы передаем анонимную функцию, которая как раз является данными.

Пример:

(function (msg) {
console.log(msg);
})('foo');

Все дело в скобках. Функция в IIFE — только часть кода. За ее вызов отвечает выражение, в которое она передается. В данном случае это скобки, но можно и по-другому:

!function (msg) {
console.log(msg);
}('foo');

+function (msg) {
console.log(msg);
}('foo');

true && function (msg) {
console.log(msg);
}('foo');

// ...

Мы уже видели, что в Питоне и Лиспе анонимные функции создаются с ключевым словом lambda. В этих языках так же можно сделать IIFE.

Питон:

(lambda: 'hello from IIFE')()

Лисп:

((lambda () "hello from IIFE"))

В этих примерах прям буквами написано: функция в IIFE — лямбда.

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

Рекурсивно суммируем числа от 1 до accumulator

const sumOf = function fn (accumulator) {
return (accumulator === 0) ? accumulator
: accumulator + fn(accumulator - 1);
};

console.log(sumOf(10)); // 55

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

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

Итоги подведем

Лямбда-исчисление — математический аппарат. Программирование — прикладной инструмент. Не стоит сильно париться по поводу соответствия терминологий, хотя теоретическую базу прокачать всегда хорошо.

В JS/Python/OCaml/… мы имеем смесь парадигм. Любой современный язык в той или иной степени поддерживает функциональное, объектно-ориентированное, структурное программирование и реально сложно находить правильные слова среди научных работ, повлиявших на язык.

Если хочется четких формулировок, стоит использовать общепринятые определения из конкретной реализации языка. В JS, например, вместо “лямбда” можно говорить “анонимная функция”, “функциональное выражение” (function expression), “именованное функциональное выражение” (named function expression). Хотя лямбда тоже сойдет, но может быть не всем понятно и некоторые, типа Эрика, могут придраться к частностям.

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

Материалы