Частичное применение функций

Roman Ponomarev
devSchacht
Published in
5 min readJun 7, 2017

Перевод статьи Functional Reactive Ninja: Partial Application of Functions.

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

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

Меня часто спрашивают: «Почему вы бы частично применили функцию?».

«Потому что логика, которую я получаю после этого, — это красота и функциональная чистота».

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

Использование function.bind()

Этот стиль частичного применения не является функционально верным, но я хочу, чтобы вы знали о нем. Пример частичного применения функций с помощью 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
  1. Мы создали функцию add, принимающую два аргумента.
  2. Мы предварительно применили ее с одним аргументом и создали функцию increment, принимающую только один аргумент.
  3. Мы точно также создали функцию incrementBy2, но применили ее с другим аргументом.
  4. Мы вызвали наши предварительно примененные функции с окончательным аргументом.

Связывание (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.

Статья на GitHub

--

--