Это пятая часть серии Функциональный JS. Перейти к предыдущей части здесь или к началу серии здесь.
Вступление
В предыдущей части этой серии мы обсуждали замыкания. Мы видели, что они позволяют нам иметь функции, возвращающие другие функции, которые запоминают переменные из своей внешней области видимости.
Мы упоминали, что это станет полезным, как только мы узнаем о частичном применении и каррировании. Пришло время вникнуть в это.
Функциональное программирование - это создание функций и использование общих функций для создания более специализированных. Это позволяет уменьшить количество дублирования и улучшить читаемость (выразительность) кода.
Частичное приложение и каррирование - это механизмы, которые мы можем использовать именно для этого - для создания специализированных версий функций поверх более общих вариантов.
Давайте посмотрим, что это такое, для чего их можно использовать и в чем разница между этими двумя концепциями.
О чем мы говорим?
Помните функции высшего порядка? Если нет, вы можете вернуться и освежить эту тему. Методы, которые мы сейчас обсудим, аналогичны функциям высшего порядка, но сделаны на один шаг дальше.
И частичное приложение, и каррирование связаны со способами вызова функций, в частности, функций, которые имеют более одного параметра. Они позволяют нам вызывать эти функции, предоставляя лишь некоторые из аргументов, оставляя остальное «на потом».
Если это сбивает с толку или звучит как «правильно, это здорово, но действительно ли это полезно» - держитесь! Мы углубимся в это, но сначала давайте взглянем на примеры, чтобы понять, о чем мы говорим.
Частичное применение
Взгляните на функцию getApiURL
и то, как мы ее используем в нескольких местах:
Как видите, здесь происходит довольно очевидное дублирование кода. getUserURL
, getOrderURL
и getProductURL
все выглядят примерно одинаково, но не совсем очевидно, как это исправить. Давай попробуем:
Так-то лучше. Здесь мы извлекли общую часть getUserURL
, getOrderURL
и getProductURL
в функцию getResourceURL
. Если подумать, общая часть - это вызов getApiURL
и передача ему https://localhost:3000
в качестве первого аргумента.
Таким образом, мы использовали более общую getApiURL
функцию для создания более специализированной getResourceURL
, которая является оболочкой. Его задача - предоставить getApiURL
первый аргумент. При последующих вызовах getResourceURL
не нужно больше беспокоиться о https://localhost:3000
части - о ней позаботятся.
Здесь мы частично применили функцию getApiURL
и создали вокруг нее функцию-оболочку getResourceURL
.
Чтобы упростить использование этого шаблона, мы можем использовать служебную функцию более высокого порядка - partial
.
Найдите минутку, чтобы понять, как partial
работает.
Посмотрите, как мы используем закрытия и оставим argsToApply
доступным к моменту вызова fn(...argsToApply, ...restArgs)
?
Сделав еще один шаг, мы могли бы частично применить getResourceURL
для создания getUserURL
и других вариантов:
Начинает хорошо выглядеть!
Интересно, что есть метод, который можно использовать точно так же, как partial
, и он встроен в сам язык! Возможно, вы даже использовали его. Это называется bind
. Он связывает функцию не только с ее this
контекстом (более популярный вариант использования), но и с ее первыми аргументами. Давайте взглянем:
Мы также можем частично применить функцию, предоставив ей более одного аргумента. В этом примере это может означать возвращение того дублирования, которого мы пытались избежать в первую очередь, но для полноты картины давайте посмотрим, как это будет выглядеть:
Каррирование
Каррирование - это техника, в чем-то похожая на частичное применение. Это также позволяет нам «исправить» некоторые параметры функции и вернуть функцию, которая «принимает» остальные.
Разница в том, что при каррировании мы передаем аргументы функции по одному.
Давайте посмотрим на пример:
Здесь мы видим, что getApiURLCurried
- это функция, которая принимает только один аргумент. Затем он возвращает функцию, которая принимает еще один аргумент и снова делает то же самое.
Чтобы использовать каррированную версию getApiURL
, нам нужно предоставить аргументы по одному: сначала apiHostname
, затем resourceName
функции, которую она возвращает, а затем, наконец, resourceId
еще одной внутренней функции.
Мы можем писать каррированные функции более кратко:
Что интересно, эта форма не намного более подробна, чем исходная версия.
В различных библиотеках есть curry
утилита, которая преобразует обычную функцию в ее каррированный эквивалент. Эта служебная функция немного сложнее, чем указанная выше служебная программа partial
, поэтому мы не будем описывать подробности ее реализации. Не стесняйтесь читать следующие источники, чтобы понять это:
- « Функциональный легкий JavaScript Кайл Симпсон»
- Https://gist.github.com/spoike/697b34a14896df4f4b8bdd9d4a89bbb1
Вот как можно использовать утилиту curry
(здесь из ramda
библиотеки):
Опять же, какая разница?
И каррирование, и частичное приложение - это шаблоны, которые позволяют нам вызывать функции с некоторыми их параметрами, а остальные предоставлять позже.
Разница в том, что:
- Частичное приложение - это более или менее образец вызова функции. Вы можете частично применить любую функцию.
Каррирование - это больше о форме функции. Чтобы иметь возможность использовать каррирование, вы должны явно создать новую функцию, которая является каррированной версией исходной. - Допустим, функция
foo
принимаетN = 5
аргументов. Частичное приложение позволяет нам вызывать его сK
аргументами и возвращать функцию, которая принимаетN - K
аргументов. Например, если мы вызовемfoo
сK = 2
аргументами, мы получим функцию, которая принимает3
аргумента. Каррирование, с другой стороны, преобразуетfoo
во вложенную цепочку функций, каждая из которых принимает1
аргумент. Вызов каррированной версииfoo
с первым аргументом вернет функцию, которая принимает второй аргумент, и возвращает функцию, которая принимает третий аргумент, и так далее ... - Как следствие вышесказанного, каррирование отличается от частичного применения тем, что происходит, если вы действительно хотите применить аргументы один за другим. С каррированными функциями вы получаете эту опцию прямо из коробки - нет необходимости использовать
curry
утилиту для данной функции более одного раза. Если вы хотите применить еще один аргумент (но не все из них) к частично примененной функции, вам нужно снова частично применить его (используяpartial
).
Но почему?
Использование частичного приложения и каррирования в коде приложения дает несколько преимуществ.
Оба они помогают нам создавать специализированные версии общих функций, тем самым устраняя дублирование и упрощая чтение и составление кода.
Но что это значит? Давайте рассмотрим несколько конкретных примеров, иллюстрирующих преимущества.
Разделение проблем
Один из них заключается в том, что при использовании этих методов вам не обязательно знать все аргументы данной функции в одном месте вашей кодовой базы. В приведенном выше примере getApiURL
одним из примеров этого может быть «нарезание» функциональности на разные уровни кода, например:
Этот пример может быть немного надуманным. Однако я пытаюсь подчеркнуть, что иногда лучше, чтобы разные «слои» вашего кода отвечали за внесение определенных параметров (которые лучше всего соответствуют их уровню абстракции) в более сложную часть бизнес-логики.
Читаемость
Еще одно преимущество использования частичного приложения и каррирования заключается в том, что они могут помочь нам создать более читаемый код. Сравните следующие версии той же функциональности - добавление 5
ко всем элементам коллекции:
Для меня использование каррированной версии add
, безусловно, наиболее элегантно. Что вы думаете?
Сочетаемость
Еще одно преимущество всей этой функциональной бессмыслицы - более легкая композиция функций. Это особенно верно для каррирования - функции, которые принимают только 1 аргумент, просто легче составить, чем другие.
Мы вернемся к композиции функций в следующих частях этой серии, а пока давайте рассмотрим простой пример, иллюстрирующий этот конкретный аспект.
Составление функций заключается в передаче результата функции непосредственно в качестве аргумента другой функции, например:
Этому примеру довольно легко следовать, но в основном потому, что и increment
, и double
являются унарными функциями (т.е. они принимают только 1 аргумент). Мы могли бы использовать такую же композицию с любой каррированной функцией - потому что все они тоже унарные.
Композиция функций не так элегантна с функциями, которые принимают более 1 аргумента. Вот почему каррированные функции легче понять, если они скомбинированы с другими функциями.
Не волнуйтесь, если вы еще не понимаете, как все это сочетается друг с другом. Мы вернемся к этому в следующих частях.
Попался!
Итак, если частичное применение и (особенно) каррирование - это так здорово, почему бы нам не использовать их все время?
Что ж, мы могли бы… :) В Haskell, например, автоматическое каррирование встроено в сам язык (ссылка на Haskell здесь не удивительна, не так ли?).
Очевидно, есть некоторые компромиссы, которые следует учитывать, прежде чем приступать к каррингу, - кроме того, что ваши коллеги ненавидят вас за то, что вы пишете код, который они не понимают.
Они выглядят как-то странно
Если вы не привыкли к этому стилю программирования, эти методы сделают определение или место вызова функции - или и то, и другое, немного неудобным («зачем вам здесь 5 пар круглых скобок ?!»).
Давайте сравним одну и ту же функциональность, написанную с использованием разных стилей:
Возможно, традиционная версия функции выглядит более знакомой. Это определенно часть кривой обучения, и все, что я могу сделать, это заверить вас: со временем она становится лучше, и через некоторое время вы можете полностью освоиться с карри-версией этой функциональности.
Порядок аргументов
Это не совсем ошибка или недостаток, но определенно ограничение как каррирования, так и частичного применения. Они не очень полезны, если вы хотите вызвать функцию, предоставляющую часть аргументов - только не первые. В таких случаях вам все равно лучше создать собственную функцию-оболочку. Давайте посмотрим на примере, чтобы прояснить это:
Если бы apiHostname
был последним параметром исходной функции getApiURL
, мы могли бы легко частично применить как resourceName
, так и resourceId
и получить функцию, которая принимала бы apiHostname
как свой («последний») параметр. Поскольку наше намерение не совсем соответствует сигнатуре функции, проще создать firstAPIUser
функцию-оболочку.
Необязательные аргументы
Еще одна вещь, которая не очень интуитивно понятна для моделирования с частичным применением и каррированием, - это необязательные параметры. Давайте рассмотрим следующий пример каррирования с использованием синтаксиса параметров по умолчанию ES6:
Это не прерывает сделку, потому что, как видите, мы можем опустить необязательный аргумент. Это просто не так элегантно - даже если нам не нужно передавать некоторые аргументы, нам все равно нужно вызывать функцию. Просто нужно иметь в виду.
Вводящие в заблуждение сообщения об ошибках
Использование каррирования может иногда приводить к тому, что наши программы выдают непонятные сообщения об ошибках, если что-то пойдет не так. Например, давайте посмотрим, что произойдет, если мы забудем передать один из аргументов функции.
Как видим, ни один из этих сценариев не особо лучше. Карри-версия взрывается перед нами с не очень описательной ошибкой. Однако традиционная версия продолжает работать, но, возможно, мы действительно предпочли бы, чтобы она потерпела неудачу.
Опять же, о чем следует помнить - использование каррированных версий функций может сделать их более «изменчивыми», но иногда это может быть хорошо.
Резюме
В этой статье мы узнали, что такое каррирование и частичное применение - способы повышения возможности повторного использования кода и улучшения читаемости. Мы также узнали, в чем разница между ними, и обнаружили их плюсы и минусы.
Таким образом, мы добавляем еще пару инструментов в ваш функциональный набор инструментов. Используйте их с умом, чтобы улучшить читаемость кода, но не забывайте об их ограничениях и недостатках.
Чтобы глубже погрузиться в эту тему, я рекомендую проверить следующее:
- « Эй, Underscore, ты делаешь это неправильно! Брайана Лонсдорфа»
- « Функциональный упрощенный JavaScript от Кайла Симпсона»
- Эта ветка StackOverflow
До встречи в следующей части!