вторник, 26 июня 2018 г.

Функциональное программирование в современном JavaScript: Частичное применение

Что такое Частичное применение?

Для того что бы создавать функции которые можно использовать повторно и создавать абстракции лучше, нам нужен механизм подстановки некоторых аргументов функции.
Частичное применение – это техника преобразования функций с высокой арностью (функции которые имеют большое число аргументов) в множество функций с меньшим количеством аргументов.

// We get the function we want to partially apply
// and the arguments that we want to bind to it
const partial = (fn, ...initialArguments) => {
 
  // Then we return a function which will take the rest
  // and call the partially applied function.
  return (...remainingArguments) => {
   
    fn(...initialArguments, ...remainingArguments);

  }
}
Частично примененная функция – это функция которая не была вызвана со всеми своими аргументами. Поскольку она ничего не может выполнить она вернет нам другую функцию которая ожидает пропущенные аргументы. После того как мы ее вызовем с недостающими данными – она выполнится.
С помощью такой техники мы попытаемся добиться большей вариативности и абстракции. Часто функции вызываются с похожим набором аргументов, и их можно переделать в частичную функцию, спрятав внутри детали реализации функции.
Карринг vs частичное применение

И карринг и частичное применение – это все техники для достижения одного результата – преобразования функций с большим количеством аргументов в функции с меньшим.
Несмотря на то, что они похожи, способы их реализации отличаются.
Функции карринга будут разбиты на много последовательных функций, каждая и которых принимает ровно один аргумент. Это делает функции карринга более предсказуемыми – вы точно знаете чего ожидать.
С другой стороны, частично примененная функция не имеет таких ограничений.Вы можете использовать любое количество аргументов и в зависимости от этого возвращаемая функция будет иметь разную степень арности. Мы только знаем что возвращаемые функции будут иметь меньше аргументов чем исходная функция.
Это две техники которые представляют одну концепцию, но отличаются способ достижения результата. Это является причиной того почему их часто путают.
Частичное применение с .bind()
Самый простой способ сделать частичное применение это использовать встроенный метод Function.prototype.bind.
Самый частый способ использования функции bind это сдоздание новой функции с префиксом this. Мы можем передать люое количество аргументов которые можно подставить в возвращаемую функцию.

const add = (x, y, z) => x + y + z;
const add10 = add.bind(null, 5, 5);
add10(7); // 17

В данном случае мы не передаем никакого значения обекту this, но мы передаем два дополнительных аргумента которые будут использоваться как x и y. После этого мы берем частично примененную функцию и вызываем ее с недостающим аргументом z.
Если быть честным, это не тот способ, который я всегда использую при работе в команде. Потому что не все знают об этом поведении, и не факт что функция названная с префиксом partial или чем то похожим будет трактована имеено так как это предполагалось дизайном.
Применение параметров по умолчанию
Ситуация, в которой частичное применение особенно полезно – это применение параметров по умолчанию.
Бывают случаи, когда мы вызываем функцию с теми же параметрами много раз. Например параметром может быть какой-то URL или объект настройки.

// This function takes three arguments. In reality
// we may be calling it in different places in our application.
// Instead of repeating the first two everywhere we can partially
// apply it and let it take only the actual data that is
// to be sent.
 
// The ajax and partial functions are not implemeneted, they
// are just used for the example.
const makePostRequest = (url, headers, data) =>
  ajax({
    url,
    headers,
    method: 'POST',
    data
  })
 
const saveComment = partial(
  makePostRequest,
  'https://example.com/post',
  { 'Content-Type': 'application/json' }
)
 
saveComment('I like this article!')
С помощью частичного применения мы можем определить функцию с повторяющимися параметрами заранее и использовать ее только для получения полезной нагрузки.
Разбитие на функции
Ингода нам требуется написать сложные функции которые принимают больше число агрументов. Как правило я стараюсь не использовать больше трех четырех аргументов, но все же иногда случается что без этого  никак, и у вас получается функция скажем с семью аргументами.
Подобная функция может быть чрезвычайно сложной в использовании, и вам придется каждый раз смотреть на ее определение, что бы понять как ей пользоваться.
Первое решение проблемы заключается в разбитии этой логики на несколько функций поменьше. Однако, часто у нас нет времени для рефакторинга или большая часть кода является зависимой от API этой функции, поэтому менять ее нельзя. В таком случае решеним может послужить частичное применение функции с использованием вспомогательной функции Function.prototype.bind, с установленными несколькими аргументами и используя возвращаемую функцию где это необходимо.

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

const arr = [1, 2, 3, 4, 5];
const compute = (x, y, z) => (x * y) + z;
 
// Let's imagine that we need to do some complex math
// with each of the numbers in the array.
 
// One way to do it would be to use an anonymous function and
// hardcode some of the coefficients that we need to use
// in the formula
arr.map(x => compute(2, 3, x)); // [7, 8, 9, 10, 11]
 
// Another way would be to partially apply the function
const addSix = compute.bind(null, 2, 3);
arr.map(x => addSix(x)) // [7, 8, 9, 10, 11]
 
// And since addSix takes a single argument now, we can clean
// up the code a little bit more.
arr.map(addSix); // [7, 8, 9, 10, 11]
Порядок аргументов
Мы не можем частично применить функцию пока не сконструируем должным образом ее первый вызов. И первое что нужно помнить здесь – это порядок аргументов, который мы будем использвать.
Другими словами мы не можем предустановить аргументы если они находятся в конце списка. Поэтому такие моменты мы должны учитывать при проектировании функций.
Некоторые библиотека, например Ramba следует соглашению, в котором каждая функция получает данные, с которыми работает в конце, таким образом функцию проще применить частично.
Однако, можно эту логику с порядком аргументов также сделать гибкой. Иногда вы наследуете код и некоторого API который не можете изменить.
Или у вас граничный случай, в котором вам необходимо изменить порядок аргументов. Ramba для таких случаев имеет функцию partialRight , которая как раз таки и предназначена для таких сценариев.

const greet = (greeting, name) => `${greeting}, ${name}!`;
 
// One way to partially apply the function is to pass a greeting
// and have the function wait to be called with a name.
const sayHello = greet.bind(null, 'Hello');
 
sayHello('Alex'); // Hello, Alex!
 
// But perhaps you need to pass the name first and use a
// different greeting depending on the time of day in which your
// user logs in. Then you can use Ramda's partialRight function.
const greetUser = R.partialRight(greet, ['Alex']);
 
greetUser('Good morning'); // Good morning, Alex!
Когда использовать частичное применение
Я не гуру в функциональном программировании. Написание статей подобного рода помогает мне так же как и тем людям, которые их читает.
Однако, если говорить о том, что я узнал, так это то, что внедрение подобных методов не является чем-то, что нужно делать принудительно.
Не существует окончательного списка, в котором расписно когда следует использовать этот прием. Особенно, когда речь идет о такой непростой теме.
Поэтому я не могу сказать, когда было бы хорошим вариантом использовать частичное приложение или когда вы обязательно должны использовать его. Это зависит от вашей команды, проекта, над которым вы работаете, и, самое главное, кода.
Если использование карринга или частичного применения ухудшит читаемость вашего кода, используйте императивный подход.
Я использовал эти методы, и я видел, насколько они полезны, когда речь заходит о повторном использовании кода. Но я также видел запутанные лица коллег, когда я решил сделать что-то необычное.
Правильный ответ на вопрос: нужно ли вам практиковать частичное приложение и карринг. И мой ответ здесь - да.
Выводы
Функциональное программирование во многом связано с тем, что мы не изучаем старые концепции, а изучаем новые. И каждый раз, когда вы играете с этими приемами, ваш ум будет тренироваться, чтобы думать иначе.
Во многих отношениях это будет путать. Вы не знаете, почему что-то работает или почему это не так. Но это хорошо - путаница означает, что вы делаете что-то новое и сложное.
Сделает ли изучение функционального программирования вас лучшим разработчиком JavaScript? Может быть. Будете ли вы использовать эти методы ежедневно? Скорее всего нет. Сделает это вас лучшим разработчиком программного обеспечения? Абсолютно!
Если вы считаете эту статью полезной, поделитесь ею с друзьями и колегами, котоыре также  интересуются подобными вопросами. Также не стесняйтесь оставлять свои коментарии.


Комментариев нет: