Однажды мой отец рассказал мне, что есть вещи, без которых легко обходишься только до тех пор, пока не приобретешь их. Микроволновки и смартфоны — как раз из таких, хотя люди постарше помнят полноценную жизнь даже без интернета. А для меня каррирование относится к таким.
Идея проста: вы можете вызвать функцию с меньшим количеством аргументов, чем она ожидает; в ответ вы получите функцию, которая принимает оставшиеся аргументы.
Таким образом, вы можете передавать функции как все аргументы сразу, так и отдельные аргументы в разные моменты времени.
const add = x => y => x + y;
const increment = add(1);
const addTen = add(10);
increment(2); // 3
addTen(2); // 12
В этом примере мы определили функцию add
, которая принимает один аргумент и возвращает новую функцию. Новая функция принимает второй аргумент (y), а также через замыкание для неё будет известен первый аргумент (x). Чтобы определять подобные функции было проще, мы воспользуемся вспомогательной функцией curry
. (При необходимости читайте подробнее о работе замыканий в JavaScript — прим. пер.).
Давайте заготовим для себя несколько каррированных функций. С этого момента мы будем подразумевать, что curry
— это та функция, которая определена для нас в Приложении A — Вспомогательные функции.
const match = curry((what, s) => s.match(what));
const replace = curry((what, replacement, s) => s.replace(what, replacement));
const filter = curry((f, xs) => xs.filter(f));
const map = curry((f, xs) => xs.map(f));
При определении этих функций я придерживался простого, но важного принципа: набор данных, с которым мы работаем (к примеру, строка или массив) я специально расположил последним аргументом. Вскоре станет ясно, почему я так сделал.
(Синтаксис /r/g
— это регулярное выражение, которое соответствует каждой букве 'r'. Прочитайте подробнее о регулярных выражениях, если интересно).
match(/r/g, 'hello world'); // [ 'r' ]
const hasLetterR = match(/r/g); // x => x.match(/r/g)
hasLetterR('hello world'); // [ 'r' ]
hasLetterR('just j and s and t etc'); // null
filter(hasLetterR, ['rock and roll', 'smooth jazz']); // ['rock and roll']
const removeStringsWithoutRs = filter(hasLetterR); // xs => xs.filter(x => x.match(/r/g))
removeStringsWithoutRs(['rock and roll', 'smooth jazz', 'drum circle']); // ['rock and roll', 'drum circle']
const noVowels = replace(/[aeiou]/ig); // (r,x) => x.replace(/[aeiou]/ig, r)
const censored = noVowels('*'); // x => x.replace(/[aeiou]/ig, '*')
censored('Chocolate Rain'); // 'Ch*c*l*t* R**n'
Здесь продемонстрирована возможность «предварительной загрузки» функции одним или несколькими аргументами для получения новой функции, которая помнит эти аргументы.
Я призываю вас склонировать репозиторий этого руководства (git clone https://github.com/MostlyAdequate/mostly-adequate-guide.git
), скопировать приведенный выше код и поэкспериментировать с ним в REPL. Функция curry
, как и всё, что описано в приложениях, доступна в модуле support/index.js
.
Вы также можете получить этот модуль при помощи npm
:
npm install @mostly-adequate/support
Каррированию есть множество применений. С помощью этой техники мы можем создавать новые функции по мере необходимости, просто предоставляя исходным функциям не все аргументы, как сделали это с hasLetterR
, removeStringsWithoutRs
и censored
.
Этим же приемом мы можем превратить любую функцию, применяемую к единственному элементу, в ту, которая будет применяться к множеству таких элементов (в частности, к массиву), просто передав её map
:
const getChildren = x => x.childNodes;
const allTheChildren = map(getChildren);
Использование функции с меньшим количеством аргументов, чем она ожидает, называется частичным применением (не следует путать частичное применение с использованием «значений аргументов по умолчанию», которое доступно в современном стандарте JavaScript; эти приемы следует считать несовместимыми и избегать совместного их использования, как следует избегать совокупного использования ФП с другими приемами, при которых арность функции не является постоянной величиной — прим. пер.).
Частичное применение функций позволяет нам избавиться от большого количества стереотипного кода. Вот как выглядела бы функция allTheChildren
с некаррированной версией map
из библиотеки lodash (обратите внимание: аргументы передаются в другом порядке):
const allTheChildren = elements => map(elements, getChildren);
Как правило, мы не станем определять функции, которые работают с массивами, потому что мы можем написать map (getChildren)
там, где потребуется. То же самое и с sort
, filter
и другими функциями высшего порядка (функция высшего порядка — такая, которая принимает или возвращает функцию).
Когда мы говорили о чистых функциях, мы говорили, что они принимают только один аргумент и возвращают одно значение. Каррирование обеспечивает именно такое поведение: с каждым аргументом возвращается новая функция, принимающая оставшиеся аргументы.
Функция считается чистой, если для одинаковых аргументов она возвращает одинаковый результат. И не принципиально, является ли он функцией. (Подразумевается, что 2 возвращаемые функции одинаковые, если они производят одинаковый результат, несмотря на то, что в результате нескольких вызовов будет возвращена не одна и та же функция. То же справедливо и для других значений-объектов — прим. пер.)
Также ради краткости мы допускаем, что каррированная функция может принять несколько аргументов сразу — чтобы в таком случае не пришлось несколько раз писать ()
(имеется в виду, что каррированная функция, строго говоря, должна принимать аргументы только по одному — прим. пер.)
Каррирование — это удобный инструмент, и мне очень нравится пользоваться им каждый день. Благодаря ему функциональное программирование становится куда менее многословным и трудоемким.
Мы можем создавать новые полезные функции на лету, просто передав пару аргументов, при этом сохраняя математическую строгость в определении функции, даже несмотря на то, что мы пользуемся функциями многих переменных.
Давайте добавим в работу ещё один инструмент — композицию
.
Глава 05: Использование композиции
На протяжении всей книги вы будете встречать раздел «Упражнения», подобный этому. Упражнения могут быть выполнены прямо в браузере, если вы читаете из gitbook (рекомендуется).
Обратите внимание, что для всех упражнений этой книги у вас всегда есть все необходимые вспомогательные функции, доступные в глобальной области видимости (актуально для online-версии). Всё, что определено в Приложении A, Приложении B и Приложении C, доступно для использования в упражнениях. Более того, некоторые упражнения также определят функции, специфичные для проблемы, которую они представляют; аналогично, считайте их доступными при выполнении упражнений.
Подсказка: вы можете проверить свое решение, нажав
Ctrl + Enter
во встроенном редакторе!
Если вы предпочитаете выполнять упражнения в файлах, используя собственный редактор:
- склонируйте репозиторий (
git clone [email protected]:MostlyAdequate/mostly-adequate-guide.git
) - перейдите в директорию exercises (
cd mostly-adequate-guide/exercises
) - установите зависимости с помощью npm (
npm install
) - выполните упражнения путем редактирования файлов с именами
exercises\_\*
в директории соответствующей главы - проверьте себя с помощью команды
npm run ch04
(указывая номер нужной главы).
Модульные тесты проверят ваши ответы и предоставят подсказки в случае ошибки. Кстати, ответы на упражнения находятся в файлах с именами answers\_\*
.
Проведите рефакторинг и избавьтесь от аргументов, используя частичное применение функции.
const words = str => split(' ', str);
Проведите рефакторинг и избавьтесь от всех аргументов путём частичного применения функций.
const filterQs = xs => filter(x => match(/q/i, x), xs);
Воспользуйтесь функцией keepHighest
:
const keepHighest = (x, y) => (x >= y ? x : y);
Проведите рефакторинг функции max
таким образом, чтобы она не нуждалась в упоминании аргументов.
const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);