Наткнулся на заметку, в которой Эрик Эллиотт рассуждает, когда анонимные функции в 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 => x * 3 (привет функции-стрелки из ES2015).

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

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

By the way, why did Church choose the notation “λ”? In [Church, 1964, §2] he stated clearly that it came from the notation “x̂” used for class-abstraction by Whitehead and Russell, by first modifying “x̂” 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обозначать связывание переменных.

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

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

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

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

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

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

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

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

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

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

Лисп:

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

Питон:

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

JS до кучи:

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

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

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

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

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

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

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

Противоречие первое: функция в IIFE не рассматривается как данные

Автор говорит, что вызываемая на месте функция (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 — лямбда.

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

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

const sumOf = function fn (accumulator) {
  // Рекурсивно суммируем числа от 1 до accumulator
  return (accumulator === 0) ? accumulator : accumulator + fn(accumulator - 1)
}
console.log(sumOf(10)) // 55

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

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

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

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

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

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

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

Материалы