Каррируй, мой блудный сын

Перевод статьи Tom Harding: Curry On Wayward Son. Опубликовано с разрешения автора.

Каррирование — в настоящий момент острая тема в функциональном JavaScript сообществе. Если вы использовали библиотеки, такие как Ramda, возможно, вы сталкивались с ним. В любом случае, я поясню, чтобы все понимали.

Функции в таких языках как Haskell или Elm принимают одно значение и возвращают одно значение, нравится вам это или нет. Если мы хотим два аргумента, мы пишем функцию, которая возвращает функцию (потому что функции тоже значения!) и передаем их:

const add = x => y => x + y // ES6

И так, для сложения 2 и 3, мы пишем add(2)(3). Каррирование функции означает превращение её из обычной записи ((x, y) => x + y) в такую. Вскоре мы увидим, что большинство из наших любимых реализаций curry больше похожи на curryish

Есть ли в этом смысл?

Да! Очевидно, писать add(2)(3) некрасиво - и мы это исправим позже, - но эта возможность не передавать все аргументы сразу еще пригодится.

Подумайте об этом: add(2) вернёт функцию, принимающую значение и прибавляющую к нему 2. Почему мы должны передавать второй аргумент сразу же? Мы можем делать различные вещи:

// 1, 2, 3, 4, 5 - Oooooo
[-1, 0, 1, 2, 3].map(add(2))

Когда мы используем функции, не получающие все их аргументы сразу же, мы называем это частичное применение. На практике, что мы делаем — это берём общую функцию (add) и специализируем её с помощью аргументов.

А вот немного более полезный (хотя еще очень надуманный) пример того, как можно обернуть String.replace, чтоб сделать более гибким:

const replace = from => to => str =>
str.replace(from, to)
const withName  = replace(/\{NAME\}/)
const withTom = withName('Tom')
const withTrump = withName('tiny hands')
const stripVowels = replace(/[aeiou]/g)('')
withTom('Hello, {NAME}!') // Hello, Tom!
withTrump('Hello, {NAME}!') // Hello, tiny hands!
stripVowels('hello') // hll
// ['hll', 'wmbldn']
['hello', 'wimbledon'].map(stripVowels)

Я не знаю как вы, но я думаю, что это реально впечатляет: мы взяли функцию и использовали частичное применение для специализации её различными путями. Вместо того, чтобы писать полностью новую функцию замены для каждого случая, мы просто частично применили некоторые её аргументы! Вот это да!

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

Но это ужасно

Да, немного некрасиво, когда вы видите такой вызов replace(/a/)('e')(str) (все эти скобочки!) в отличии от replace(/a/, 'e', str), но мы не хотим быть вынуждены писать все аргументы сразу.

Что мы действительно любим, так это писать эти аргументы группируя как нам надо:

replace(/a/)('e')(str)
== replace(/a/, 'e')(str)
== replace(/a/)('e', str)
== replace(/a/, 'e', str)

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

const uncurryish = f => {
if (typeof f !== 'function')
return f // Needn't curry!
  return (... xs) => uncurryish(
xs.reduce((f, x) => f (x), f)
)
}

Может, немного коряво, но суть такова:

  • Все значения, не являющиеся функциями, будут возвращаться целыми и невредимыми.
  • Функции обернуты в функцию, которая принимает один или несколько аргументов, применяет их по одному, затем возвращает результат в uncurryish.

Это всё ужасная рекурсия опять! Если вы определите функции replace и uncurryish как раньше, вы увидите, всё работает. Ура!

Подожди, uncurry? Я хотел curry!

Ну, нет, это не uncurry (он просто выглядит немного похоже, но я понял вас). Когда вы используете что-то типа Ramda curry, они означают curryish. Единственное реальное различие между curryish и uncurryishэто то, что curryish начинает с «нормальной» функции (например (x, y) => x + y), а uncurryish начинает с функции как в этой статье. Конечный результат одинаковый, также uncurryish имеет релизацию гораздо проще*… Используете ли вы одно или другое —полностью зависит от вас!

В любом случае, я надеюсь, это прольет свет. Я думал, что это может быть проще начать с uncurry, а потом изменять его, пока он не будет соответствовать curry, к которому мы привыкли. Все, что вам нужно знать, это то что curry и uncurryish достигают одного результата: они собирают аргументы функции пока не наберут нужное количество для её запуска и потом возвращают результат функции.

Это действительно отличный трюк и вы можете проделать невероятный рефакторинг. Конечно, если что-то не так, напишите мне tweet или еще что, и я постараюсь, прояснить!

Большое спасибо за чтение!

Берегите себя ♥

* Не совсем справедливо — Ramda делает некоторые другие вещи, такие как заполнители, но это определенно сложнее из-за необходимости отслеживать «состояние» аргумента явно.


Слушайте наш подкаст в iTunes и SoundCloud, читайте нас на Medium, контрибьютьте на GitHub, общайтесь в группе Telegram, следите в Twitter и канале Telegram, рекомендуйте в VK и Facebook.

Статья на Github