Skip to content

Latest commit

 

History

History
151 lines (95 loc) · 13.9 KB

ch04-ru.md

File metadata and controls

151 lines (95 loc) · 13.9 KB

Глава 04: Каррирование

Без тебя мне жизнь не мила

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

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

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

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 git@github.com:MostlyAdequate/mostly-adequate-guide.git)
  • перейдите в директорию exercises (cd mostly-adequate-guide/exercises)
  • установите зависимости с помощью npm (npm install)
  • выполните упражнения путем редактирования файлов с именами exercises\_\* в директории соответствующей главы
  • проверьте себя с помощью команды npm run ch04 (указывая номер нужной главы).

Модульные тесты проверят ваши ответы и предоставят подсказки в случае ошибки. Кстати, ответы на упражнения находятся в файлах с именами answers\_\*.

Давайте попрактикуемся!

Упражнение A

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

const words = str => split(' ', str);  

Упражнение B

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

const filterQs = xs => filter(x => match(/q/i, x), xs);

Упражнение C

Воспользуйтесь функцией keepHighest:

const keepHighest = (x, y) => (x >= y ? x : y);  

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

const max = xs => reduce((acc, x) => (x >= acc ? x : acc), -Infinity, xs);