Каррирование и композиция функций в Javascript

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

Интересный факт: Каррирование названо в честь математика Haskell Curry, а не еда :)

Что такое каррированная функция?

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

Но прежде чем мы начнем, давайте кратко поговорим о функциях в JavaScript. Есть два способа определения JS-функций: объявления и выражения.

// function declaration 
function addOne(foo) {
   return foo + 1 
}
// function expression 
var addOne = function(foo) {   
return foo + 1 
}

Объявления функций - это именованные функции, вызываемые с помощью ключевого слова function, а выражения функций - это анонимные функции, которые назначаются переменной (* let, const, var). Самое главное различие между ними - подъем. В JavaScript переменные (* все они поднимаются в верхнюю часть своей области видимости, но в то время как varvariables инициализируются с помощью undefined, let и const переменные не инициализируются.) и function операторы поднимаются в верхнюю часть своей области видимости, что означает, что с ними можно взаимодействовать до того, как они будут объявлены.

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

Введение в стрелочные функции

Стрелочные функции - это краткий способ создания функциональных выражений, представленных в ES6. Я буду использовать const при создании стрелочных функций, а не avar, это не является абсолютно необходимым, но в целом использование const с выражениями функций, которые вы не собираетесь переназначать, является хорошей практикой в ​​ES6.

Стрелочные функции выполняют ряд вещей автоматически за кулисами, например, лексическая привязка this (что очень важно!).

// function expression 
var addOne = function(foo) {   
return foo + 1
}
// equivalent arrow function
const addOne = (foo) => foo + 1
// equivalent arrow function with optional syntax added 
const addOne = (foo) => {   
return foo + 1 
}

Сложность управления

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

const addOneToEach = arr => arr.map(num => num + 1)
addOneToEach([1, 2, 3]) // returns [2, 3, 4]

Итак, мы объявили стрелочную функцию и массив. Наша функция addOneToEach принимает массив как единственный параметр. Выполняет итерацию по массиву, увеличивая каждое число в массиве на 1 и возвращая [2, 3, 4].

Array.prototype.map(). .map() принимает функцию обратного вызова с аргументами (currentValue, index, array). Но в нашем случае нас заботило только currentValue

Хорошее начало! Но что, если бы мы использовали API и имели дело с массивами, предоставляемыми через API. Не все API хороши (на самом деле всего несколько). Данные, которые мы будем извлекать, в большинстве случаев не будут согласованными, это могут быть числа или строки. Нам нужно будет добавить некоторую логику для обнаружения строк и их отмены.

const addOneToEach = arr => arr.map(data => {
  if (typeof data === "string") {  
    return parseInt(data) + 1   
  }   
  return data + 1  
})
addOneToEach([1, 2, 3])    // returns [2, 3, 4]  
addOneToEach([1, "2", 3])  // returns [2, 3, 4]

Это работает, но все стало немного запутаться. Мы уже начали встраивать логику в три уровня.

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

const addOneToEach = arr => 
  arr.map(data => typeof data === "string" 
  ? parseInt(data) + 1 
  : data + 1 
)

Хороший! Но мы не можем где-либо повторно использовать нашу хорошо продуманную логику, а также не можем экспортировать или тестировать ее изолированно. Лучшей практикой в ​​этом случае было бы разделение + 1 и parseInt на отдельные именованные функции и использование их в качестве отдельных обратных вызовов внутри связанных вызовов arr.map().

const ensureNum = data => 
  typeof data === "string" ? parseInt(data) : data
const addOne = num => num + 1
const addOneToEach = arr => arr.map(ensureNum).map(addOne)

Отличная работа! Мы разделили логику на подлогики и создали индивидуальные и компактные стрелочные функции; ensureNum и addOne, что позволяет легко использовать их повторно, экспортировать и тестировать. Мы используем эти функции как обратные вызовы в связанном .map() вызове в нашей addOneToEach функции.

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

const ensureNum = data =>   
  typeof data === "string" 
  ? parseInt(data) 
  : data
const addOne = num => num + 1 
const addThree = num => num + 3
const addTen = num => num + 10
 
const addOneToEach = arr =>   arr.map(ensureNum).map(addOne)
const addThreeToEach = arr =>   arr.map(ensureNum).map(addThree)
const addTenToEach = arr =>   arr.map(ensureNum).map(addTen)

Я имею в виду, что, конечно, мы можем сделать это сложным способом, просто кодируя альтернативные функции, такие как addTheToEach addTenToEach, но мы не хотим этого делать, мы хотим делать это СУХОЙ способ! давайте добавим новый параметр by для хранения числа, на которое мы увеличиваем.

const ensureNum = data =>   
  typeof data === "string" ? parseInt(data) : data
const incrementEach = (arr, by) => 
  arr.map(ensureNum)
     .map(num => num + by)

Это намного лучше, но мы потеряли обратный вызов с именем addOne. Давайте создадим новую addNums стрелочную функцию, в которую мы можем передать как текущий num в .map(), так и новый by параметр из incrementEach в.

const ensureNum = data=> 
  typeof data === "string" ? parseInt(data) : data
const addNums = (a, b) => a + b
const incrementEach = (arr, by) => 
  arr.map(ensureNum) 
     .map(num => addNums(num, by))

Давайте добавим карри, о котором я говорил!

Давайте на небольшой перерыв забудем о том, что мы пытались сделать до сих пор, и поговорим о каррировании в действии!

//Using function declaration syntax
function add(a, b){
  return a + b;
}
add(2, 3); // returns 5
//Using arrow functions syntax
const add = (a, b) => {
  return a + b;
}
add(2, 3); // returns 5

Теперь у нас есть функция, которая принимает два аргумента, a и b, и возвращает сумму a и b. Теперь мы собираемся каррировать эту функцию:

//Using function declaration syntax
function add (a) {
  return function (b) {
    return a + b;
  }
}
//Using arrow functions syntax
const add = a => b => a+b;

Как видите, у нас есть функция с именем add, которая принимает один аргумент, a, и возвращает анонимную функцию, которая принимает другой аргумент, b, и возвращает сумму a и b.

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

add(2)(3);
const add2 = add(2);
add2(3);

Выше первая инструкция возвращает 5, как и инструкция add (2, 3). Второй оператор определяет новую функцию с именем add2, которая добавит 2 к своему аргументу. Это то, что некоторые называют закрытием. Третий оператор использует функцию add2, чтобы добавить 3 к 2, чтобы в результате получить 5.

Или просто для дополнительной сложности и демонстрационных целей мы можем связать другую функцию для тройного эффекта ...

//Using function expression syntax
var sum = function(x, y, z){
 return x + y + z;
}
console.log(sum(1, 2, 3)); // 6
var sum = function sum(x) {
  return function(y) {
    return function(z) {
      return x + y + z;
    };
  };
};
console.log(sum(1)(2)(3)); // 6
//Using arrow functions syntax
//Using arrow functions syntax
const sum = (x, y, z) => x + y + z;
console.log(sum(1, 2, 3)); // 6
const sum = x => y => z => x + y + z;
console.log(sum(1)(2)(3)); // 6

Как видите, на самом фундаментальном уровне ничего не меняется.

Мы вернулись туда, где мы оставили

Вернемся к нашей incrementEach функции. Вот где мы оставили вещи:

const ensureNum = data=> 
  typeof data === "string" ? parseInt(data) : data
const addNums = (a, b) => a + b
const incrementEach = (arr, by) => 
  arr.map(ensureNum) 
     .map(num => addNums(num, by))

А теперь давайте снова напишем их каррированием!

const ensureNum = data =>   
  typeof data === "string" ? parseInt(data) : data
const addNums = a => b => a + b
const incrementEach = (arr, by) =>   
  arr.map(ensureNum)     
     .map(addNums(by))

addNums теперь является функцией, которая принимает параметр, который возвращает функцию, которая принимает другой параметр. Итак, сначала мы вызываем addNums(by), который возвращает анонимную функцию b => by + b. .map() затем вызывает эту анонимную функцию в качестве обратного вызова и возвращает by + currentValue - текущее значение в массиве, увеличенное на значение by.

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

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