☕ Каррирование и функции высшего порядка в JavaScript за 5 простых шагов
Улучшаем производительность и увеличиваем возможность повторного использования JavaScript кода с помощью функционального программирования. Каррирование и функции высшего порядка – это основы функционального программирования. JavaScript поддерживает их «из коробки», а значит, мы просто обязаны использовать эти возможности для создания чистого высокопроизводительного кода, который легко переиспользовать. Одна из важных особенностей функционального программирования на JavaScript основана на том, что функции в языке являются «гражданами первого класса» (first-class citizens). Это означает, что вы можете делать с ними все, что придет в голову, например, передавать их как аргументы другим функциями или возвращать из других функций. По сути вызов Следующий фрагмент делает то же самое, но более многословно. Посмотрим на еще один пример, в котором функция выступает как параметр другой функции: Функция То же самое можно сделать и с Выражение Единственная разница между двумя функциями идентичности В функциональном программировании функция высшего порядка (higher-order function) – это самая обычная функция, которая оперирует другими функциями: принимает их как входные параметры или возвращает в качестве выходных. Таким образом, все функции из предыдущего примера – это функции высшего порядка. Вложенные функции (одна функция создается внутри другой) всегда имеют доступ к области видимости родительской функции, которая невидима снаружи. Таким образом, функция возвращенная из другой функции, может работать с данными, которые больше никому не видны. Это называется замыканием. Это упрощенная реализация функции мемоизации. Давайте разбираться, что здесь происходит. Мемоизация – это техника оптимизации вычислений в функциональном программировании. Она сохраняет результаты предыдущих вычислений в виде ассоциативного массива (пары ключ-значение) и использует их многократно. Например, у нас есть функция, которая приводит строку к верхнему регистру: Мы хотим мемоизировать ее (предположим, что изменение регистра – это очень трудозатратная операция). Мы передаем эту функцию в функцию высшего порядка Новая функция работает точно так же, как и старая: она принимает строку и возвращает ту же строку в верхнем регистре. Но под капотом используется техника мемоизации. Если входные данные повторяются, вычисление не происходит заново. Вместо этого берется ранее сохраненное значение из кеша Используемый стиль кода, при котором промежуточные переменные опускаются, называется point free. Это своего рода «конвейер», при котором значение, возвращенное одной функцией, сразу же используется как входной параметр для следующей функции. Внутренняя функция приводит строку к верхнему регистру, а memo сразу использует возвращенный ей результат. Вам тоже кажется, что каррирование – это что-то из кулинарии? 🙂 На самом деле это техника преобразования функций. Свое название она получила в честь математика Хаскелла Карри (Haskell Curry). Да-да, это тот же самый человек, в честь которого назвали язык программирования Haskell. Его исследования комбинаторной логики вместе с лямбда-исчислением Алонсо Черча (Alonzo Church) легли в основу функционального программирования как парадигмы. На первом шаге мы уже составляли сложные выражения с несколькими круглыми скобками для последовательного вызова функций. Там одна функция возвращала другую, которую мы не записывали в переменную, а сразу же вызывали. Каррирование выглядит точно так же: Функции Каждая стрелка в При этом используется входящий аргумент первой функции – Второй вызов (вторые круглые скобки) вернет уже результат выполнения вложенной функции: Каррирование – это способ пошагового получения последовательности аргументов. В В функциональном программировании предпочтительно создавать унарные функции, которые имеют всего один аргумент. Из них уже составляются функции с большей арностью. Например, бинарная функция Каррированные функции очень удобны, так как могут быть легко переиспользованы в разных комбинациях для создания других функций. В этом примере мы используем три унарные функции Такой стиль написания кода следует лучшим практикам программирования: KISS, DRY и принципу единой ответственности. Этот код легко тестировать, потому что он состоит из отдельных независимых друг от друга кусочков, которые работают без побочных эффектов. Каррировать JavaScript-функции при каждом использовании – занятие раздражающее, поэтому лучше использовать уже готовую библиотеку со множеством утилит, например, @7urtle/lambda. Помимо прочего она предоставляет полезную функцию Вы передаете ей несколько функций в качестве аргументов, через запятую, а она выстраивает из них конвейер. При этом выполнение функций будет осуществляться справа налево, то есть функция, которую вы передадите последней, выполнится первой, а ее результат будет использован предпоследней функцией. Обратите внимание, как изящно и коротко (всего в одной строчке кода) нам удалось определить функцию Функциональное программирование – это настоящая магия, но чтобы овладеть ей, вам потребуется немного практики (в первое время в скобочках легко запутаться). Альтернативный способ сделать то же самое – функция Она написана в императивном стиле и занимает целых 17 строк (515 символов против 115 у декларативного варианта)! Вероятно, вы уже задумались о поиске или создании волшебной функции, которая автоматически каррирует вашу n-арную функцию, или позволит вызывать каррированную функцию в привычном стиле с несколькими аргументами. Тогда вам нужны функции Обратите внимание, что сами функции *** Познав магию функционального программирования на JavaScript, вы не захотите возвращаться обратно. Это очень простой и элегантный способ создания кода, который нельзя не оценить.Шаг 1. Функции первого класса
// простая JS-функция const firstClassType = input => input; // возвращаем функцию из другой функции const myFunctionAsOutput = function () { return firstClassType; }; firstClassType('value'); // => 'value' myFunctionAsOutput()('value'); // => 'value'
firstClassType
– это пример функции идентичности, которая принимает некоторое значение в качестве аргумента и его же возвращает, не изменяя.myFunctionAsOutput
не принимает никаких параметров, а просто возвращает переменную firstClassType
, которая содержит функцию. myFunctionAsOutput()
– это то же самое, что и прямое обращение к firstClassType
– эти выражения взаимозаменяемы. Поэтому мы можем вызвать результат работы myFunctionAsOutput()
как обычную функцию с помощью круглых скобок.
const intermediateFn = myFunctionAsOutput(); intermediateFn('value');
// принимаем функцию как аргумент в другую функцию и возвращаем ее const myFunctionAsInputAndOutput = function(inputFn) { return inputFn; }; myFunctionAsInputAndOutput(firstClassType)('value'); // => 'value'
myFunctionAsInputAndOutput
принимает функцию и ее же возвращает. По сути это просто еще один пример функции идентичности. То есть вызов myFunctionAsInputAndOutput(firstClassType)
– это то же самое, что и прямое обращение к firstClassType
, значит, мы тоже можем его вызвать.firstClassType
, если в качестве параметра input
передать функцию.
firstClassType(firstClassType)('value'); // => 'value'
firstClassType(firstClassType)
возвращает саму функцию firstClassType
, которую вновь можно вызвать 🙂firstClassType
и myFunctionAsInputAndOutput
состоит в том, что первая – это стрелочная функция, а вторая – обычная. Шаг 2. Функции высшего порядка
const memo = fn => { // переменная memory находится в области видимости функции memo // и не видна снаружи let memory = []; return anything => { // область видимости вложенной функции if(anything in memory) { // вложенная функция читает переменную memory return memory[anything]; } else { const result = fn(anything); // вложенная функция изменяет переменную memory memory[anything] = result; return result; } }; }; const upperCase = memo(a => a.toUpperCase()); upperCase('7urtle'); // => '7URTLE'
a => a.toUpperCase()
memo
и получаем взамен другую функцию, которую сохраняем в переменную upperCase
.
const upperCase = memo(a => a.toUpperCase());
memory
.Шаг 3. Каррирование функций в JavaScript
const curry1 = function (a) { return function (b) { return a + b; }; }; const curry2 = a => b => a + b; curry2(5); // => b => 5 + b; curry1(2)(3) === curry2(2)(3); // => 5
curry1
и curry2
записаны по-разному, но работают абсолютно одинаково. Это сделано для наглядности, чтобы вы не запутались в стрелочных функциях. curry2
возвращает выражение, которое стоит справа от нее. Первая стрелка (первый вызов функции) возвращает вложенную функцию:
curry2(5); // b => 5 + b
a
.
curry2(5)(10); // 15
curry2
всего два параметра (и две стрелки), но вы можете использовать сколько угодно при необходимости (только не забывайте о читаемости кода).
// один аргумент const unary = a => a; // два аргумента // без каррирования const binary = (a, b) => a + b; // с каррированием const curriedBinary = a => b => a + b; // три аргумента // без каррирования const ternary = (a, b, c) => a + b + c; // с каррированием const curriedTernary = a => b => c => a + b + c; ternary(1, 2, 3) === curriedTernary(1)(2)(3); // => 6
curriedBinary
составлена из двух унарных.Шаг 4. Переиспользование каррированных функций
const filter = checker => list => list.filter(checker); const lowerCaseOf = input => input.toLowerCase(); const includes = what => where => where.includes(what); const isTortoise = input => includes('tortoise')(lowerCaseOf(input)); const filterTortoises = filter(isTortoise); const turtles = ['Greek Tortoise', 'Green Turtle']; filterTortoises(turtles); // => ['Greek Tortoise']
filter
, lowerCaseOf
и includes
. Из них мы собираем сложные функции isTortoise
и filterTortoises
, как конструктор Lego из отдельных деталек.Шаг 5. Улучшенная композиция функций
compose
для удобной композиции нескольких функций.
// без compose input => includes('tortoise')(lowerCaseOf(input)) // с compose compose(includes('tortoise'), lowerCaseOf)
import {lowerCaseOf, includes, filter, compose, memo} from '@7urtle/lambda'; // мемоизированный конвейер из функций includes и lowerCaseOf const isTortoise = memo(compose(includes('tortoise'), lowerCaseOf)); const filterTortoises = filter(isTortoise); const turtles = ['Greek Tortoise', 'Green Turtle']; filterTortoises(turtles); // => ['Greek Tortoise']
isTortoise
, которая на самом деле выполняет большой объем работы. И все это практически без потери читаемости. imperativeFilterTortoises
.
const imperativeFilterTortoises = function(turtles) { let memory = []; let tortoises = []; for (let i = 0; i < turtles.length; i++) { let isTortoise; if(turtles[i] in memory) { isTortoise = memory[turtles[i]]; } else { isTortoise = turtles[i].toLowerCase().includes('tortoise'); memory[turtles[i]] = isTortoise; } if(isTortoise) { tortoises.push(turtles[i]); } } return tortoises; }; imperativeFilterTortoises(turtles); // => ['Greek Tortoise']
Бонус: функции curry и nary
curry
и nary
. Первая принимает n-арную функцию, вторая – каррированную. Обе позволяют вызывать полученные функции в любом стиле (как каррированные или как n-арные).
const curry = fn => (...args) => args.length >= fn.length ? fn(...args) : (...args2) => curry(fn)(...args, ...args2); const nary = fn => (...args) => args.length === 0 ? fn() : args.reduce( (accumulator, current) => accumulator(current), fn ); const fromBinary = curry((a, b) => a + b); const fromCurried = nary(a => b => a + b); fromBinary(2, 3) === fromBinary(2)(3); // => true fromCurried(2, 3) === fromCurried(2)(3); // => true fromBinary(2, 3) === fromCurried(2, 3); // => true
curry
и nary
написаны в декларативном функциональном стиле. Они обе доступны в библиотеке @7urtle/lambda. Фактически, библиотека под капотом использует функцию nary
, чтобы обеспечить возможность вызывать пользовательские функции любым удобным способом.
- 7 views
- 0 Comment