Skip to content

Latest commit

 

History

History
 
 

tom-harding-curry-on-wayward-son

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

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

Перевод статьи 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.

Статья на Medium