Перевод статьи Functional Reactive Ninja: Partial Application of Functions.
Предоставление функции с меньшим количеством аргументов, чем она ожидает, называется частичным применением функций.
Хотя концепция проста, её можно использовать для подготовки более сильных функциональных конструкций в нашем повседневном JavaScript.
Меня часто спрашивают: «Почему вы бы частично применили функцию?».
«Потому что логика, которую я получаю после этого, - это красота и функциональная чистота».
Мы вызываем функцию с меньшим количеством аргументов, чем она ожидает, и она возвращает функцию, принимающую остальные аргументы. Это и называется частичным применением функций.
Этот стиль частичного применения не является функционально верным, но я хочу, чтобы вы знали о нем. Пример частичного применения функций с помощью bind()
:
let add = (a, b) => a+b;
let increment = add.bind(null,1);
let incrementBy2 = add.bind(null,2);
console.log('Increment 3 by 2:',incrementBy2(3));
//=> Увеличиваем 3 на 2: 5
console.log('Increment 3 by 1:',increment(3));
//=> Увеличиваем 3 на 1: 4
- Мы создали функцию
add
, принимающую два аргумента. - Мы предварительно применили её с одним аргументом и создали функцию
increment
, принимающую только один аргумент. - Мы точно также создали функцию
incrementBy2
, но применили её с другим аргументом. - Мы вызвали наши предварительно примененные функции с окончательным аргументом.
Связывание (binding) функции с меньшим количеством аргументов помогает нам генерировать другие функции, чтобы сделать наш код менее повторяющимся и более конкретным. Но у этого подхода существуют проблемы и связаны они с тем, что это функционально неверно:
- Слишком непредсказуемо:
function.bind
всегда возвращает другую функцию, даже если мы предоставили все аргументы базовой функции. Поэтому мы не знаем, когда остановиться.
- Обратите внимание, что в коде используется
null
- это контекст частично применяемой функции, который мы должны передать в качестве первого аргументаbind
. Каждый раз, когда мы частично применяем функцию, мы вынуждены присоединять контекст - не круто!
Это потрясающая техника функционального программирования, которая может быть достигнута в JavaScript из-за его способности создавать функции высшего порядка. Каррирование не является частичным применением функции, но помогает в достижении той же цели более функционально. Каррированная версия нашей функции add()
:
let add = x => y => x+y;
let increment = add(1);
let incrementBy2 = add(2);
console.log('Increment 3 by 1:',increment(3));
//=> Увеличиваем 3 на 1: 4
console.log('Increment 3 by 2:',incrementBy2(3));
//=> Увеличиваем 3 на 2: 5
- Предсказуемо: каррированная функция сделана так, что она всегда возвращает другую функцию, принимающую только один аргумент.
- Потрясающе: каррированная функция всегда запоминает применяемые аргументы из-за замыкания. И все это выглядит круто, когда написано как лямбда-выражение. 😎
- Чисто: каррированная функция всегда чиста, так как она генерирует одну и ту же функцию для одних и тех же входных данных.
Каррированная функция всегда чиста, так как она генерирует одну и ту же функцию для одних и тех же входных данных
Каррирование - это пояс с крутыми фишками Бэтмена для функционального программиста, и мы увидим, насколько оно незаменимо.
Теперь я хочу, чтобы вы взглянули на этот код.
‘Hello’.replace(/Hello/g, ‘Bye’).concat(‘!’);
Эта конструкция называется чейнинг методов (method chaining) и, как известно, это хороший объектно-ориентированный подход к проектированию. Теперь, если вы посмотрите внимательней, вы заметите, как он работает, и чего ему не хватает.
Данные Hello
передаются по цепочке совместимых методов, и мы можем использовать сколько угодно методов, пока возвращаемый объект совместим с ними.
Мы можем продолжать, например, так:
‘Hello’
.replace(/Hello/g, ‘Bye’)
.concat(‘!’)
.repeat(2)
.split('!')
.filter(x=>x!='!')
.map(x=>'Hello').toString();
Вышеупомянутая конструкция возможна из-за объекта Hello
: все методы в цепочке бесполезны без объекта, предоставляющего их. Это грустно. Проблема конкретно здесь и с каждым объектно-ориентированным подходом к проектированию, что все крутится вокруг объектов, все зависит от данных.
Давайте перейдем к функциональному подходу:
const replace = (regex,replacement,str) => str.replace(regex,replacement);
const concat = (item,str) => str.concat(item);
concat('!',replace(/Hello/g,'Bye','Hello'));
Выглядит менее читаемым, но не волнуйтесь - это функциональное программирование. Для таких случаев у нас есть композиция функций.
Композиция функций - это процесс объединения двух или более функций для создания новой функции.
Мы можем использовать композицию функций для объединения наших функций replace
и concat
. Тогда нам нужна функция-композер:
const compose = (...fns) => x => fns.reduce((v, fn) => fn(v), x);
Давайте объединим наши функции, но, подождите, у нас есть проблема: наша функция-композер работает с функциями, которые принимают только один параметр, а наши функции replace
и concat
явно принимают более одного параметра.
Время для нашей любимой техники - каррирования:
const replace = (regex,replacement) => str => str.replace(regex,replacement);
const concat = item => str => str.concat(item);
Теперь мне нужно, чтобы вы внимательно изучили, как я стратегически каррировал свои функции, чтобы единственным аргументом, оставшимся для применения, были «данные».
В итоге:
compose(replace(/Hello/g,’Bye’),concat(‘!’))(‘Hello’)
Мы можем продолжать, например, так:
compose(
replace(/Hello/g,’Bye’),
concat(‘!’),
repeat(2),
split('!'),
filter(x=>x!='!'),
map(x=>'Hello'),
toString
)(‘Hello’)
// или
processHello(‘Hello’)
И это круто по всем параметрам! Например, поскольку наши функции больше не зависят от предоставленных данных, мы можем сделать так:
[‘Hello’,’Hello world’,’Hi’].map(processHello)
Я извиняюсь, я имел ввиду вот так:
map(processHello)(‘Hello’,’Hello world’,’Hi’)
Поздравляю, мы только что написали функцию в бесточечном стиле. Вы не видите «.».
Функции в бесточечном стиле - функции, не упоминающие данные, которыми они оперируют. Этот стиль записи функций называется бесточечным стилем (point-free) или комбинаторным программированием (tacit programming). Согласно Википедии:
Комбинаторное программирование, также называемое бесточечным, представляет собой парадигму программирования, не требующую явного упоминания аргументов определяемой функции и использующая вместо переменных комбинаторы и композиции.
Каррирование и композиция очень хорошо подходят для программирования в таком стиле.
Каррирование подготавливает функции, чтобы принимать только данные в качестве аргумента (остальные аргументы частично применяются заранее), а композиция помогает в объединении этих частично примененных функций, чтобы данные могли проходить через них.
И этот бесточечный стиль написания функций - лакмусовая бумажка функциональной чистоты. Поскольку он подтверждает, что наш код состоит из маленьких чистых функций, иначе мы бы не смогли их компоновать.
Смотрите, результаты чистые и это потому, что вы применяли функции по частям.
Представьте себе Бэтмена без его пояса. Функциональный Javascript просто невозможен без каррирования, поэтому популярные библиотеки функционального программирования, такие как lodash/fp или Ramda, поставляются с уже каррированными функциями.
Написано 💖.
Спасибо за прочтение.
Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.