Это пятая часть серии Функциональный 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, поэтому мы не будем описывать подробности ее реализации. Не стесняйтесь читать следующие источники, чтобы понять это:

Вот как можно использовать утилиту curry (здесь из ramda библиотеки):

Опять же, какая разница?

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

Разница в том, что:

  1. Частичное приложение - это более или менее образец вызова функции. Вы можете частично применить любую функцию.
    Каррирование - это больше о форме функции. Чтобы иметь возможность использовать каррирование, вы должны явно создать новую функцию, которая является каррированной версией исходной.
  2. Допустим, функция foo принимает N = 5 аргументов. Частичное приложение позволяет нам вызывать его с K аргументами и возвращать функцию, которая принимает N - K аргументов. Например, если мы вызовем foo с K = 2 аргументами, мы получим функцию, которая принимает 3 аргумента. Каррирование, с другой стороны, преобразует foo во вложенную цепочку функций, каждая из которых принимает 1 аргумент. Вызов каррированной версии foo с первым аргументом вернет функцию, которая принимает второй аргумент, и возвращает функцию, которая принимает третий аргумент, и так далее ...
  3. Как следствие вышесказанного, каррирование отличается от частичного применения тем, что происходит, если вы действительно хотите применить аргументы один за другим. С каррированными функциями вы получаете эту опцию прямо из коробки - нет необходимости использовать curry утилиту для данной функции более одного раза. Если вы хотите применить еще один аргумент (но не все из них) к частично примененной функции, вам нужно снова частично применить его (используя partial).

Но почему?

Использование частичного приложения и каррирования в коде приложения дает несколько преимуществ.

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

Но что это значит? Давайте рассмотрим несколько конкретных примеров, иллюстрирующих преимущества.

Разделение проблем

Один из них заключается в том, что при использовании этих методов вам не обязательно знать все аргументы данной функции в одном месте вашей кодовой базы. В приведенном выше примере getApiURL одним из примеров этого может быть «нарезание» функциональности на разные уровни кода, например:

Этот пример может быть немного надуманным. Однако я пытаюсь подчеркнуть, что иногда лучше, чтобы разные «слои» вашего кода отвечали за внесение определенных параметров (которые лучше всего соответствуют их уровню абстракции) в более сложную часть бизнес-логики.

Читаемость

Еще одно преимущество использования частичного приложения и каррирования заключается в том, что они могут помочь нам создать более читаемый код. Сравните следующие версии той же функциональности - добавление 5 ко всем элементам коллекции:

Для меня использование каррированной версии add, безусловно, наиболее элегантно. Что вы думаете?

Сочетаемость

Еще одно преимущество всей этой функциональной бессмыслицы - более легкая композиция функций. Это особенно верно для каррирования - функции, которые принимают только 1 аргумент, просто легче составить, чем другие.

Мы вернемся к композиции функций в следующих частях этой серии, а пока давайте рассмотрим простой пример, иллюстрирующий этот конкретный аспект.

Составление функций заключается в передаче результата функции непосредственно в качестве аргумента другой функции, например:

Этому примеру довольно легко следовать, но в основном потому, что и increment, и double являются унарными функциями (т.е. они принимают только 1 аргумент). Мы могли бы использовать такую ​​же композицию с любой каррированной функцией - потому что все они тоже унарные.

Композиция функций не так элегантна с функциями, которые принимают более 1 аргумента. Вот почему каррированные функции легче понять, если они скомбинированы с другими функциями.

Не волнуйтесь, если вы еще не понимаете, как все это сочетается друг с другом. Мы вернемся к этому в следующих частях.

Попался!

Итак, если частичное применение и (особенно) каррирование - это так здорово, почему бы нам не использовать их все время?

Что ж, мы могли бы… :) В Haskell, например, автоматическое каррирование встроено в сам язык (ссылка на Haskell здесь не удивительна, не так ли?).

Очевидно, есть некоторые компромиссы, которые следует учитывать, прежде чем приступать к каррингу, - кроме того, что ваши коллеги ненавидят вас за то, что вы пишете код, который они не понимают.

Они выглядят как-то странно

Если вы не привыкли к этому стилю программирования, эти методы сделают определение или место вызова функции - или и то, и другое, немного неудобным («зачем вам здесь 5 пар круглых скобок ?!»).

Давайте сравним одну и ту же функциональность, написанную с использованием разных стилей:

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

Порядок аргументов

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

Если бы apiHostname был последним параметром исходной функции getApiURL, мы могли бы легко частично применить как resourceName, так и resourceId и получить функцию, которая принимала бы apiHostname как свой («последний») параметр. Поскольку наше намерение не совсем соответствует сигнатуре функции, проще создать firstAPIUser функцию-оболочку.

Необязательные аргументы

Еще одна вещь, которая не очень интуитивно понятна для моделирования с частичным применением и каррированием, - это необязательные параметры. Давайте рассмотрим следующий пример каррирования с использованием синтаксиса параметров по умолчанию ES6:

Это не прерывает сделку, потому что, как видите, мы можем опустить необязательный аргумент. Это просто не так элегантно - даже если нам не нужно передавать некоторые аргументы, нам все равно нужно вызывать функцию. Просто нужно иметь в виду.

Вводящие в заблуждение сообщения об ошибках

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

Как видим, ни один из этих сценариев не особо лучше. Карри-версия взрывается перед нами с не очень описательной ошибкой. Однако традиционная версия продолжает работать, но, возможно, мы действительно предпочли бы, чтобы она потерпела неудачу.

Опять же, о чем следует помнить - использование каррированных версий функций может сделать их более «изменчивыми», но иногда это может быть хорошо.

Резюме

В этой статье мы узнали, что такое каррирование и частичное применение - способы повышения возможности повторного использования кода и улучшения читаемости. Мы также узнали, в чем разница между ними, и обнаружили их плюсы и минусы.

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

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

До встречи в следующей части!