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

Обновлено: 06.07.2024

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

Время чтения: больше 15 мин

  1. Функция как элемент композиции
    1. Чистые функции и побочные эффекты
    2. Рекурсия
    3. Функции высших порядков
    4. Частичное применение
    5. Каррирование
    1. Функциональное ядро в императивной оболочке
    2. Контейнеры результатов
    3. Паттерн-матчинг
    1. Надёжность и удобство тестирования
    2. Оптимизация при компиляции
    3. Параллелизм и потокобезопасность
    1. Повышенное потребление памяти
    2. Сложность при работе с нечистыми сервисами

    Обновлено 27 октября 2021

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

    Код становится плохим, когда он перестаёт соответствовать реальности — бизнес-логике, правилам поведения частей системы, их отношениям друг с другом. Бизнес-логика — это территория. Код — карта этой территории. Чем точнее карта, тем проще справляться с изменениями в требованиях и даже предвидеть их.

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

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

    Функция как элемент композиции

    Любая система состоит из частей. Программы — тоже системы со своими целями и средствами достижения этих целей. Сопоставление разных частей системы вместе называется композицией, а эти части — элементами композиции (composition units).

    Композиция — одна из важнейших проблем в программировании. Грамотное сопоставление частей делает программу расширяемой и отказоустойчивой.

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

    • Границы должны быть проведены так, чтобы элемент решал только одну проблему, а не несколько (принцип разделения ответственности).
    • Элементы ничего не должны знать об устройстве других элементов, а общаться они должны через данные (закон Деметры).
    • Данные и настройки должны быть отделены от кода программы (The Twelve-Factor App).

    В функциональном программировании элемент композиции — это функция. Передача данных через несколько вызовов функций — их композиция. Например, если мы хотим к числу 10 прибавить 5, а потом умножить результат на 42, то последовательно вызовем функции add и multiply :

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

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

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

    Чистые функции и побочные эффекты

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

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

    Чистые функции всегда при вводе одинаковых аргументов выдают одинаковый результат. По этому свойству легко отличить чистую функцию от нечистой.

    Например, pureFn при вводе 10 и 20 всегда будет возвращать 15, значит она чистая:

    А impureFn нечистая — она будет возвращать разные значения, потому что использует случайное число:

    И alsoImpureFn тоже нечистая:

    Дело в том, что мы не знаем, как именно устроены методы random и now в объектах снаружи. Они могут не только возвращать результат, но и менять состояние окружающего мира, например, меняя какую-то переменную.

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

    Рекурсия

    Так как в функциональном программировании нельзя менять состояние, то для итеративных процессов мы не можем применять циклы. Вместо этого нам нужно использовать отображение ( map ) и свёртку ( reduce ) или рекурсию.

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

    Функции высших порядков

    Иногда нам попадаются почти одинаковые задачи, которые отличаются только деталями. Например, нам может быть нужно достать из массива только положительные числа или только чётные числа. Мы могли бы написать нечто вроде:

    Мы бы могли перебор вариантов превратить в другую функцию, в которую бы передавали массив и условие проверки:

    Здесь новая функция filter , которая непосредственно перебирает значения. Она принимает на вход массив и функцию-предикат, которая проверяет каждое значение массива по своему условию.

    Реализуем filter самостоятельно, чтобы понять, как всё работает. Объявим функцию filter , в которую передадим два аргумента: массив и функцию, проверяющую условие — предикат.

    Внутри создадим пустой массив, который будем наполнять подходящими под условие элементами, а в конце — вернём как результат:

    Каждый элемент переданного массива мы передадим в функцию-предикат, и если она вернёт true , добавим этот элемент в массив-результат:

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

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

    Вообще, в JavaScript filter уже есть, поэтому мы можем переписать код вот так:

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

    Частичное применение

    Например, есть функция умножения multiply , но мы хотим дополнительно создать ещё и удвоитель double . Например, потому что он используется в программе чаще другого умножения.

    Решением в лоб было бы просто написать ещё одну функцию:

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

    Тогда создать удвоитель мы сможем, написав:

    По сути равно этому:

    Таким же образом мы можем создать и утроитель и множитель на 10:

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

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

    Каррирование

    Каррирование – это трансформация функций таким образом, чтобы они принимали аргументы не как f(a, b, c) , а как f(a)(b)(c) . То есть это буквально то же, что мы сделали с функцией multiply , только автоматизировано.

    Попробуем сделать это в лоб:

    Вроде просто, но если аргументов будет больше 2, то придётся добавлять ещё одну обёртку. Поэтому лучше посчитать количество аргументов и автоматизировать создание обёрток:

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

    Теперь мы можем как применить функцию частично, так и выполнить сразу, если потребуется:

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

    Работа с побочными эффектами

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

    А вот это уже труъ:

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

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

    • Запись данных в базу;
    • Получение данные от API;
    • Запрос к сети за картинкой.

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

    Функциональное ядро в императивной оболочке

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

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

    • сперва мы запрашиваем и получаем данные из базы, то есть производим побочный эффект;
    • затем преобразовываем данные с помощью чистой функции;
    • после записываем данные в базу, то есть снова производим побочный эффект.

    Получается такой сэндвич: побочный-эффект, чистое преобразование, побочный-эффект:

    Почипо-сэндвич: побочный эффект, чистое преобразование, побочный эффект

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

    В строгой парадигме всё несколько сложнее

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

    Контейнеры результатов

    Обычно в JavaScript ошибки обрабатывают императивно с помощью try-catch :

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

    Сравним два способа обрезать строку, привести её к числу и прибавить единицу:

    Мы можем реализовать собственный контейнер, операции с которым тоже можно будет компоновать с помощью map .

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

    Теперь мы можем соединять преобразования с помощью map — то есть использовать композицию:

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

    Нам понадобится ещё один метод — fold , который сможет достать из замыкания функции Box нужное значение и вернуть его:

    Тогда достать значение с его помощью мы сможем так:

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

    Результат будет с типом Ok :

    Ошибка будет с типом Error :

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

    Проблема этой функции в том, что мы не знаем, как обрабатывать её результат: это может быть или строка, или undefined . То есть следующий код приведёт к ошибке:

    С контейнером же мы не беспокоиться о случае с undefined :

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

    В примере выше если x — строка, к нему применится 3 преобразования из map и в конце на экране появится alert . В случае если x === undefined , то преобразования будут проигнорированы.

    Подробнее о том, что такое контейнеры, почему они играют такую важную роль, и как с их помощью обрабатывать исключения, посмотрите в курсе Professor Frisby Introduces Composable Functional JavaScript на Egghead.

    Паттерн-матчинг

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

    Концептуально он похож на switch:

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

    В JavaScript тоже можно (хоть и с костылями) использовать предикаты для паттерн-матчинга. Мы можем проверять результат выражений прямо в case :

    Но обычно, чтобы использовать паттерн-матчинг в JavaScript, подключают дополнительные библиотеки.

    Математические основы

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

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

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

    Если вам хочется по-настоящему узнать, что такое функторы, монады, семигруппы и прочее, то советуем прочитать спецификацию Fantasy Land. Будьте аккуратны, там сложно.

    Плюсы функционального программирования

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

    Надёжность и удобство тестирования

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

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

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

    Оптимизация при компиляции

    Параллелизм и потокобезопасность

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

    Минусы функционального программирования

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

    Повышенное потребление памяти

    Сложность при работе с нечистыми сервисами

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

    @niki-timofe, это слабое оправдание. Если вы не приложите усилий, чтобы задать хороший вопрос, то не можете ожидать, что мы приложим усилия, чтобы дать хороший ответ. Таким образом вам придется удовлетвориться копипастой из ссылки выше, что несколько глупо для программиста, ибо подразумевает выполнение машинной работы вручную.

    4 ответа 4

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

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

    Здесь в качестве параметра x выступает другая лямбда-функция λy.y + 1 , в которую передаётся параметр 2. То есть любая лямбда функция является функцией высшего порядка, может принимать в качестве аргумента другую функцию и возвращать функцию:

    Сложность составляет только явное указание типа аргументов и возвращаемого значения.

    Стрелочные функции (arrow functions) появились в 6-й редакции Javascript, более известной как ECMAScript 6. Сегодня их можно встретить практически в любом современном приложении, написанном на любом из современных JS фреймворков.

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

    • Имеют более короткий и понятный синтаксис.
    • Позволяют использовать, так называемый, 'implicit return' (неявный) и писать функции с одной инструкцией в одну строчку
    • Принимает значение this из внешней функции (не создает собственный контекст исполнения)

    Давайте разберем эти преимущества более детально.

    Предположим у нас есть массив данных:

    Давайте используем метод .map() , чтобы пробежаться по нашему массиву, добавить к каждому значению слово 'fresh' и получить новый массив freshFruit :

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

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

    Cтрелочные Функции JS

    Для начала давайте уберем слово function и добавим символ стрелочной функции => . Таким образом мы получим выражение следующего вида:

    Наша функция принимает один единственный аргумент – fruit . Следовательно, мы можем избавиться от скобок вокруг него.

    Также мы можем использовать так называемый implicit return (подразумеваемое возвращение…). Явное возвращение (explicit return) подразумевает использование слова return при возврате результата. Если ваша функция возвращает какой-то один результат, то написание слова return можно опустить.

    В результате получаем:

    Что мы изменили?

    • Удалили слово return
    • Записали все выражение в одну строчку
    • Удалили фигурные <> скобки

    После удаления слова return и фигурных скобок мы получаем implicit return, и нам не требуется уточнять, что наша функция возвращает fresh $ (это предполагается).

    Что eсли стрелочная функция не принимает никаких аргументов?

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

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

    Лично я предпочитаю использовать () => вместо (_) => , когда в функцию не требуется передовать аргументы.

    Все стрелочные функции - безымянные

    Для начала, приведу пример функции с указанием названия:

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

    Стрелочные функции являются анонимными, то есть им нельзя присвоить название.

    С другой стороны, стрелочную функцию можно присвоить переменной. В этом примере мы объявляем стрелочную функцию и присваиваем ее переменной, используя function expression:

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

    Значение this в стрелочных функциях

    Внутри обычной функции значение this зависит от того, как была вызвана функция.

    Например, при обычном выполнении функции, значение this - глобальный объект.

    Если вы вызываем функцию объекта, то значением this является сам объект.

    У стрелочных функций отсутствует собственный контекст выполнения, следовательно, значение this в стрелочных функциях всегда наследуется от родительской (внешней) функции.

    В этом примере мы получим undefined для каждого числа в массиве numbers:

    Значение this , которое мы получаем внутри console.log(this.message, number) определяется обычной функцией. Эта функция не привязана к нашему объекту, поэтому получаем - undefined.

    Второй вариант - использовать стрелочную функцию.

    Значение this внутри обыкновенной функции - зависит от контекста вызова. Значение this в стрелочной функции всегда принимает значение внешней функции!

    Что такое лямбда в мире компьютерных наук для человека без опыта работы в области компьютерных технологий?

    см это для того, как лямбда - функции выглядят на разных языках, и в каком году они были введены в этих языках.

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

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

    питон

    Как видно из фрагмента Python, функция adder принимает аргумент x и возвращает анонимную функцию или лямбду, которая принимает другой аргумент y. Эта анонимная функция позволяет вам создавать функции из функций. Это простой пример, но он должен передать силу лямбды и замыкания.

    Примеры на других языках

    Perl 5

    JavaScript

    JavaScript (ES6)

    Схема

    стриж

    PHP

    Haskell

    Ява увидеть этот пост

    Lua

    Котлин

    Рубин

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

    Ruby - это Ruby, для ламбд есть сокращение, поэтому вы можете определить adder это следующим образом:

    р

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

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

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

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

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

    Я бы предложил Console.WriteLine("Calculator: op " + op.Method.Name + " (" + a + ", " + b + ") text-muted font-weight-lighter"> — 23

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

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

    Простой пример (используя Scala для следующей строки):

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

    за исключением того, что вам не нужно беспокоиться о:

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

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

    вместо того, чтобы просто написать выражение, где оно вам нужно:

    Точная запись варьируется от языка к языку; Греческий не всегда требуется! ;-)

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

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

    Это может быть применено в строке, как это (оценивается до 50 ):

    Лямбда-исчисление является последовательной математической теорией замещения. В школьной математике можно увидеть, например, в x+y=5 паре с x−y=1 . Наряду со способами манипулирования отдельными уравнениями также возможно объединить информацию из этих двух, при условии, что подстановки между уравнениями выполняются логически. Лямбда-исчисление кодифицирует правильный способ сделать эти замены.

    Учитывая, что y = x−1 это допустимая перестановка второго уравнения, это: λ y = x−1 означает функцию, заменяющую символы x−1 для символа y . Теперь представьте себе применение λ y к каждому члену в первом уравнении. Если термин - y тогда выполнить замену; иначе ничего не делать. Если вы сделаете это на бумаге, вы увидите, как применять это λ y сделает первое уравнение разрешимым.

    Это ответ без какой-либо информатики или программирования.

    Вот как квадратная функция может быть определена в императивном языке программирования (C):

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

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

    лямбда

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

    LinqToSql может прочитать эту функцию (x> 15) и преобразовать ее в фактический SQL для выполнения с использованием деревьев выражений.

    Заявление выше становится:

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

    Теперь дерево выражений не может быть прочитано - SomeComplexCheck не может быть разбит. Оператор SQL будет выполняться без где и каждая строка в данных будет пропущена SomeComplexCheck .

    Лямбда-функции не следует путать с анонимными методами. Например:

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

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