Введение в функциональное программирование на JavaScript

Spread the love

Когда Брендан Эйч создал JavaScript в 1995 году, он намеревался внедрить язык программирования Scheme в браузер. Scheme, будучи диалектом Lisp, является функциональным языком. Ситуация изменилась, когда Эйху сказали, что новый язык должен быть языком похожим на Java. В конечном итоге Эйх остановился на языке, который имеет синтаксис в стиле C (как и Java), но имеет функции первого класса (first-class functions). Технически Java не имела первоклассных функций до версии 8, однако вы могли моделировать первоклассные функции, используя анонимные классы. Эти первоклассные функции делают функциональное программирование возможным в JavaScript.

JavaScript – это мультипарадигмальный язык, который позволяет свободно смешивать и сопоставлять объектно-ориентированные, процедурные и функциональные парадигмы. В последнее время наблюдается тенденция к функциональному программированию. В таких фреймворках, как Angular и React, вы получите повышение производительности за счет использования неизменяемых структур данных. Неизменность является основным принципом функционального программирования. Это, наряду с чистыми функциями, облегчает анализ и отладку ваших программ. Замена процедурных циклов функциями может улучшить читаемость вашей программы и сделать ее более элегантной. В целом, у функционального программирования есть много преимуществ.

Чем функциональное программирование не является

Прежде чем говорить о том, что такое функциональное программирование, давайте поговорим о том, чем оно не является. На самом деле, давайте поговорим обо всех языковых конструкциях, которые вы должны выбросить (до свидания, старые друзья): программирование и разработка

  • Циклы
    • while
    • do…while
    • for
    • for…of
    • for…in
  • Объявление переменных с помощью var или let
  • Функции Void
  • Изменяемость объекта (например: o.x = 5;)
  • Методы массива
    • copyWithin
    • fill
    • pop
    • push
    • reverse
    • shift
    • sort
    • splice
    • unshift
  • Методы Map
    • clear
    • delete
    • set
  • Методы Set
    • add
    • clear
    • delete

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

Чистые функции

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

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

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

function multiply(a, b) {
  return a * b;
}

Ниже приведены примеры грязных функций. Функция canRide зависит от захваченной переменной heightRequirement. Захваченные переменные не обязательно делают функцию грязной, но изменяемые (или переназначаемые) делают. В этом случае heightRequirement была объявлена с использованием let, что означает, что она может быть переназначена. Функция multiply в этом примере грязная, потому что она вызывает побочный эффект выводом в консоль.

let heightRequirement = 46;

// Impure because it relies on a mutable (reassignable) variable.
function canRide(height) {
  return height >= heightRequirement;
}

// Impure because it causes a side-effect by logging to the console.
function multiply(a, b) {
  console.log('Arguments: ', a, b);
  return a * b;
}

Следующий список содержит несколько встроенных в JavaScript грязных функций. Можете ли вы указать, какое из двух свойств не удовлетворяет каждое из них?

  • console.log
  • element.addEventListener
  • Math.random
  • Date.now
  • $.ajax (где $ == какая нибудь библиотека Ajax)

Жить в идеальном мире, в котором все наши функции чисты, было бы неплохо, но, как видно из приведенного выше списка, любая значимая программа будет содержать грязные функции. Большую часть времени нам нужно будет сделать Ajax-вызов, проверить текущую дату или получить случайное число. Хорошее практическое правило – следовать правилу 80/20: 80% ваших функций должны быть чистыми, а оставшиеся 20% по необходимости будут грязными.

Есть несколько преимуществ для чистых функций:

  • Их легче отлаживать, потому что они не зависят от изменяемого состояния.
  • Возвращаемое значение может быть кэшировано или «запомнено», чтобы избежать его повторного вычисления в будущем.
  • Их проще тестировать, потому что нет никаких зависимостей (таких, как ведение журнала, Ajax, база данных и т. д.), которые необходимо проверять.

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

Неизменность

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

let heightRequirement = 46;

function canRide(height) {
  return height >= heightRequirement;
}

// Каждые полсекунды назначаем heightRequirement случайное число от 0 до 200.
setInterval(() => heightRequirement = Math.floor(Math.random() * 201), 500);

const mySonsHeight = 47;

// Каждые полсекунды проверяем, может ли мой сын кататься.
// Иногда это будет правдой, а иногда - ложью.
setInterval(() => console.log(canRide(mySonsHeight)), 500);

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

const heightRequirement = 46;

function canRide(height) {
  return height >= heightRequirement;
}

Объявление переменной с помощью const означает, что нет никаких шансов, что она будет переназначена. Если будет предпринята попытка переназначить ее, механизм выполнения выдаст ошибку; однако, что если вместо простого числа у нас будет объект, в котором хранятся все наши «константы»?

const constants = {
  heightRequirement: 46,
  // ... other constants go here
};

function canRide(height) {
  return height >= constants.heightRequirement;
}

Мы использовали const, поэтому переменная не может быть переназначена, но проблема остается. Объект может быть видоизменен. Как показано в следующем коде, для достижения истинной неизменности вам нужно предотвратить переназначение переменной, а также вам нужны неизменяемые структуры данных. Язык JavaScript предоставляет нам метод Object.freeze для предотвращения изменения объекта.

'use strict';

// CASE 1: The object is mutable and the variable can be reassigned.
let o1 = { foo: 'bar' };

// Mutate the object
o1.foo = 'something different';

// Reassign the variable
o1 = { message: "I'm a completely new object" };


// CASE 2: The object is still mutable but the variable cannot be reassigned.
const o2 = { foo: 'baz' };

// Can still mutate the object
o2.foo = 'Something different, yet again';

// Cannot reassign the variable
// o2 = { message: 'I will cause an error if you uncomment me' }; // Error!


// CASE 3: The object is immutable but the variable can be reassigned.
let o3 = Object.freeze({ foo: "Can't mutate me" });

// Cannot mutate the object
// o3.foo = 'Come on, uncomment me. I dare ya!'; // Error!

// Can still reassign the variable
o3 = { message: "I'm some other object, and I'm even mutable -- so take that!" };


// CASE 4: The object is immutable and the variable cannot be reassigned. This is what we want!!!!!!!!
const o4 = Object.freeze({ foo: 'never going to change me' });

// Cannot mutate the object
// o4.foo = 'talk to the hand' // Error!

// Cannot reassign the variable
// o4 = { message: "ain't gonna happen, sorry" }; // Error

Неизменность относится ко всем структурам данных, включая массивы, map и set. Это означает, что мы не можем вызывать методы-мутаторы, такие как array.prototype.push, потому что это изменяет существующий массив. Вместо того, чтобы помещать элемент в существующий массив, мы можем создать новый массив со всеми теми же элементами, что и исходный массив, плюс один дополнительный элемент. Фактически, каждый метод мутатора может быть заменен функцией, которая возвращает новый массив с желаемыми изменениями.

'use strict';

const a = Object.freeze([4, 5, 6]);

// Instead of: a.push(7, 8, 9);
const b = a.concat(7, 8, 9);

// Instead of: a.pop();
const c = a.slice(0, -1);

// Instead of: a.unshift(1, 2, 3);
const d = [1, 2, 3].concat(a);

// Instead of: a.shift();
const e = a.slice(1);

// Instead of: a.sort(myCompareFunction);
const f = R.sort(myCompareFunction, a); // R = Ramda

// Instead of: a.reverse();
const g = R.reverse(a); // R = Ramda

// Exercise for the reader:
// copyWithin
// fill
// splice

То же самое происходит при использовании Map или Set. Мы можем избежать использования методов-мутаторов, возвращая новую Map или Set с желаемыми изменениями.

const map = new Map([
  [1, 'one'],
  [2, 'two'],
  [3, 'three']
]);

// Instead of: map.set(4, 'four');
const map2 = new Map([...map, [4, 'four']]);

// Instead of: map.delete(1);
const map3 = new Map([...map].filter(([key]) => key !== 1));

// Instead of: map.clear();
const map4 = new Map();
const set = new Set(['A', 'B', 'C']);

// Instead of: set.add('D');
const set2 = new Set([...set, 'D']);

// Instead of: set.delete('B');
const set3 = new Set([...set].filter(key => key !== 'B'));

// Instead of: set.clear();
const set4 = new Set();

Я хотел бы добавить, что если вы используете TypeScript (я большой поклонник TypeScript), то вы можете использовать интерфейсы Readonly<T> , ReadonlyArray<T> , ReadonlyMap<K, V> и ReadonlySet<T> для получения ошибки во время компиляции, если вы попытаетесь изменить любой из этих объектов. Если вы вызовите Object.freeze для литерала объекта или массива, то компилятор автоматически определит, что он доступен только для чтения. Из-за того, что Maps и Sets внутренние данные, вызов Object.freeze для этих структур данных не работает одинаково. Но достаточно просто сказать компилятору, что вы хотите, чтобы они были доступны только для чтения.

TypeScript Readonly Interfaces

TypeScript read-only interfaces

Итак, мы можем создавать новые объекты вместо того, чтобы изменять существующие, но не скажется ли это на производительности? Да, оно может. Обязательно проведите тестирование производительности в своем собственном приложении. Если вам нужно повысить производительность, подумайте об использовании Immutable.js. Immutable.js реализует Lists, Stacks, Maps, Sets и другие структуры данных, используя постоянные структуры данных. Это тот же метод, который используется внутри функциональных языков программирования, таких как Clojure и Scala.

// Use in place of `[]`.
const list1 = Immutable.List(['A', 'B', 'C']);
const list2 = list1.push('D', 'E');

console.log([...list1]); // ['A', 'B', 'C']
console.log([...list2]); // ['A', 'B', 'C', 'D', 'E']


// Use in place of `new Map()`
const map1 = Immutable.Map([
  ['one', 1],
  ['two', 2],
  ['three', 3]
]);
const map2 = map1.set('four', 4);

console.log([...map1]); // [['one', 1], ['two', 2], ['three', 3]]
console.log([...map2]); // [['one', 1], ['two', 2], ['three', 3], ['four', 4]]


// Use in place of `new Set()`
const set1 = Immutable.Set([1, 2, 3, 3, 3, 3, 3, 4]);
const set2 = set1.add(5);

console.log([...set1]); // [1, 2, 3, 4]
console.log([...set2]); // [1, 2, 3, 4, 5]

Композиция функций

Помните еще в старших классах, когда вы узнали что-то похожее (f ∘ g)(x)? Помните, когда вы думали: «Когда я смогу это использовать?» Вот теперь, самое время. Готовы? f ∘ g читается как «f, составленный с помощью g». Есть два эквивалентных способа мышления об этом, как показано этим равенством: (f ∘ g) (x) = f (g (x)). Вы можете думать о f ∘ g как о единственной функции или как о результате вызова функции g, а затем о ее выводе и передаче его в f. Обратите внимание, что функции применяются справа налево, то есть мы выполняем g, а затем f.

Несколько важных моментов о композитных функций:

  1. Мы можем составить любое количество функций (мы не ограничены двумя).
  2. Один из способов составления функций – просто взять выходные данные из одной функции и передать их следующей (то есть f(g(x))).
// h(x) = x + 1
// number -> number
function h(x) {
  return x + 1;
}

// g(x) = x^2
// number -> number
function g(x) {
  return x * x;
}

// f(x) = convert x to string
// number -> string
function f(x) {
  return x.toString();
}

// y = (f ∘ g ∘ h)(1)
const y = f(g(h(1)));
console.log(y); // '4'

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

// h(x) = x + 1
// number -> number
function h(x) {
  return x + 1;
}

// g(x) = x^2
// number -> number
function g(x) {
  return x * x;
}

// f(x) = convert x to string
// number -> string
function f(x) {
  return x.toString();
}

// R = Ramda
// composite = (f ∘ g ∘ h)
const composite = R.compose(f, g, h);

// Execute single function to get the result.
const y = composite(1);
console.log(y); // '4'

Итак, мы можем сделать композицию функций в JavaScript. Подумаешь? Что ж, если вы действительно работаете с функциональным программированием, то в идеале вся ваша программа будет не более чем композицией функций. В вашем коде не будет циклов (forfor…offor…inwhiledo). Но это невозможно, говорите вы! Не так. Это приводит нас к следующим двум темам: рекурсия и функции высшего порядка.

Рекурсия

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

n! = n * (n-1) * (n-2) * … * 1.

То есть n! является произведением всех целых чисел от n до 1. Мы можем написать цикл, который вычисляет это для нас достаточно легко.

function iterativeFactorial(n) {
  let product = 1;
  for (let i = 1; i <= n; i++) {
    product *= i;
  }
  return product;
}

Обратите внимание, что оба product и i неоднократно переназначаются внутри цикла. Это стандартный процедурный подход к решению проблемы. Как бы мы решили это, используя функциональный подход? Нам нужно устранить цикл и убедиться, что у нас нет переназначаемых переменных. Рекурсия – один из самых мощных инструментов в наборе инструментов функционального программиста. Рекурсия просит нас разбить общую проблему на подзадачи, которые напоминают общую проблему.

Вычисление факториала – прекрасный пример. Чтобы вычислить n!, нам просто нужно взять n и умножить его на все меньшие целые числа. Это то же самое, что сказать:

n! = n * (n-1)! 

Ага! Мы нашли подзадачу для решения (n-1)! и это напоминает общую проблему n!. Есть еще одна вещь, о которой нужно позаботиться: базовый вариант. Базовый случай говорит нам, когда прекратить рекурсию. Если бы у нас не было базового варианта, рекурсия продолжалась бы вечно. На практике вы получите ошибку переполнения стека, если будет слишком много рекурсивных вызовов.

Каков базовый случай для факториальной функции? Сначала вы можете подумать, что это когда n == 1, но из-за некоторых сложных математических вещей, это когда n == 00! определяется как 1. Имея в виду эту информацию, давайте напишем рекурсивную факториальную функцию.

function recursiveFactorial(n) {
  // Base case -- stop the recursion
  if (n === 0) {
    return 1; // 0! is defined to be 1.
  }
  return n * recursiveFactorial(n - 1);
}

Хорошо, давайте вычислим recursiveFactorial(20000), потому что … ну почему бы и нет! Когда мы сделаем, мы получаем это:

Stack overflow error

Stack overflow error

Так что здесь происходит? Мы получили ошибку переполнения стека! Это не из-за бесконечной рекурсии. Мы знаем, что обработали базовый вариант (то есть n === 0). Это случилось потому, что браузер имеет конечный стек, и мы его превысили. Каждый вызов recursiveFactorial приводит к тому, что в стек помещается новый вызов. Мы можем визуализировать стек как набор блоков, сложенных друг на друга. Каждый раз, когда вызывается recursiveFactorial, в верхнюю часть добавляется новый блок. Следующая диаграмма показывает стилизованную версию того, как может выглядеть стек при вычислении recursiveFactorial(3). Обратите внимание, что в реальном стеке верхний фрейм будет хранить адрес памяти, куда он должен вернуться после выполнения, но я решил изобразить возвращаемое значение с помощью переменной r. Я сделал это, потому что разработчикам JavaScript обычно не нужно думать об адресах памяти.

The stack for recursively calculating 3! (three factorial)

The stack for recursively calculating 3! (three factorial)

Вы можете представить, что стек для n = 20000 будет намного выше. Мы можем с этим что-нибудь сделать? Оказывается, да, мы можем с этим что-то сделать. В рамках спецификации ES2015 (также известной как ES6) была добавлена оптимизация для решения этой проблемы. Это называется правильной оптимизацией вызовов (PTC). Она позволяет браузеру исключать или пропускать стековые фреймы, если последнее, вызывает себя (это то что делает рекурсивная функция, ) и возвращает результат. На самом деле, оптимизация работает и для взаимно рекурсивных функций, но для простоты мы просто сосредоточимся на одной рекурсивной функции.

Вы заметите в стеке выше, что после рекурсивного вызова функции еще предстоит выполнить дополнительное вычисление (т. е. n * r). Это означает, что браузер не может оптимизировать его с помощью PTC; однако мы можем переписать функцию таким образом, чтобы последним шагом был рекурсивный вызов. Один из способов сделать это – передать промежуточный результат (в данном случае product) в функцию в качестве аргумента.

'use strict';

// Optimized for tail call optimization.
function factorial(n, product = 1) {
  if (n === 0) {
    return product;
  }
  return factorial(n - 1, product * n)
}

Давайте теперь представим оптимизированный стек при вычислении factorial(3). Как показано на следующей диаграмме, в этом случае стек никогда не выходит за пределы двух кадров. Причина в том, что мы передаем всю необходимую информацию (т. е. product) в рекурсивную функцию. Итак, после обновления product браузер может выбросить этот кадр стека. На этой диаграмме вы видите, что каждый раз, когда верхний кадр падает и становится нижним, предыдущий нижний кадр отбрасывается. Это больше не нужно.

The optimized stack for recursively calculating 3! (three factorial) using PTC

The optimized stack for recursively calculating 3! (three factorial) using PTC

Теперь запустите это в браузере по вашему выбору, и, предположив, что вы запустили его в Safari, вы получите ответ, который является Infinity (это число больше, чем максимальное представимое число в JavaScript). Но мы не получили ошибку переполнения стека, так что это хорошо! А как насчет других браузеров? Оказывается, Safari – единственный браузер, в котором реализована PTC, и он может быть единственным браузером, который когда-либо его реализовывал. Смотрите следующую таблицу совместимости:

PTC compatibility

PTC compatibility

Другие браузеры выдвинули конкурирующий стандарт, называемый синтаксическими хвостовыми вызовами syntactic tail calls (STC). «Синтаксический» означает, что вы должны использовать новый синтаксис, для функций которая должна участвовать в оптимизации хвостового вызова. Несмотря на то, что пока еще нет широкой поддержки браузеров, все равно хорошей идеей будет написание ваших рекурсивных функций, чтобы они были готовы к оптимизации хвостового вызова всякий раз, когда это происходит.

Функции высшего порядка

Мы уже знаем, что в JavaScript есть первоклассные функции (first-class), которые можно передавать, как и любое другое значение. Поэтому неудивительно, что мы можем передать функцию другой функции. Мы также можем вернуть функцию из функции. Вуаля! У нас есть функции высшего порядка. Вы, наверное, уже знакомы с несколькими функциями более высокого порядка, которые существуют в Array.prototype. Например, filter, map и reduce, среди других. Один из способов представить себе функцию более высокого порядка: это функция, которая передается в виде параметров, что обычно называется функцию обратного вызова. Давайте рассмотрим пример использования встроенных функций высшего порядка:

const vehicles = [
  { make: 'Honda', model: 'CR-V', type: 'suv', price: 24045 },
  { make: 'Honda', model: 'Accord', type: 'sedan', price: 22455 },
  { make: 'Mazda', model: 'Mazda 6', type: 'sedan', price: 24195 },
  { make: 'Mazda', model: 'CX-9', type: 'suv', price: 31520 },
  { make: 'Toyota', model: '4Runner', type: 'suv', price: 34210 },
  { make: 'Toyota', model: 'Sequoia', type: 'suv', price: 45560 },
  { make: 'Toyota', model: 'Tacoma', type: 'truck', price: 24320 },
  { make: 'Ford', model: 'F-150', type: 'truck', price: 27110 },
  { make: 'Ford', model: 'Fusion', type: 'sedan', price: 22120 },
  { make: 'Ford', model: 'Explorer', type: 'suv', price: 31660 }
];

const averageSUVPrice = vehicles
  .filter(v => v.type === 'suv')
  .map(v => v.price)
  .reduce((sum, price, i, array) => sum + price / array.length, 0);

console.log(averageSUVPrice); // 33399

Обратите внимание, что мы вызываем методы для объекта массива, что характерно для объектно-ориентированного программирования. Если бы мы хотели сделать это немного более представительным для функционального программирования, мы могли бы вместо этого использовать функции, предоставляемые Ramda или lodash/fp. Мы также можем использовать композицию функций, которую мы рассмотрели в предыдущем разделе. Обратите внимание, что нам нужно изменить порядок функций, если мы используем R.compose, так как он применяет функции справа налево (то есть снизу вверх); однако, если мы хотим применить их слева направо (то есть сверху вниз), как в примере выше, то мы можем использовать R.pipe.. Оба примера приведены ниже с использованием Ramda. Обратите внимание, что у Ramda есть функция mean, которую можно использовать вместо reduce.

const vehicles = [
  { make: 'Honda', model: 'CR-V', type: 'suv', price: 24045 },
  { make: 'Honda', model: 'Accord', type: 'sedan', price: 22455 },
  { make: 'Mazda', model: 'Mazda 6', type: 'sedan', price: 24195 },
  { make: 'Mazda', model: 'CX-9', type: 'suv', price: 31520 },
  { make: 'Toyota', model: '4Runner', type: 'suv', price: 34210 },
  { make: 'Toyota', model: 'Sequoia', type: 'suv', price: 45560 },
  { make: 'Toyota', model: 'Tacoma', type: 'truck', price: 24320 },
  { make: 'Ford', model: 'F-150', type: 'truck', price: 27110 },
  { make: 'Ford', model: 'Fusion', type: 'sedan', price: 22120 },
  { make: 'Ford', model: 'Explorer', type: 'suv', price: 31660 }
];

// Using `pipe` executes the functions from top-to-bottom. 
const averageSUVPrice1 = R.pipe(
  R.filter(v => v.type === 'suv'),
  R.map(v => v.price),
  R.mean
)(vehicles);

console.log(averageSUVPrice1); // 33399

// Using `compose` executes the functions from bottom-to-top.
const averageSUVPrice2 = R.compose(
  R.mean,
  R.map(v => v.price),
  R.filter(v => v.type === 'suv')
)(vehicles);

console.log(averageSUVPrice2); // 33399

Преимущество подхода функционального программирования состоит в том, что он четко отделяет данные (то есть vehicles) от логики (то есть функции filter, map и reduce). Сравните это с объектно-ориентированным кодом, который смешивает данные и функции в форме объектов с методами.

Карринг

Неформально каррирование – это процесс получения функции, которая принимает n аргументов, и превращения ее в n функций, каждая из которых принимает один аргумент. Арность функции – это число аргументов, которые она принимает. Функция, которая принимает один аргумент, является унарной, два аргумента двоичными, три аргумента троичными, а n аргументов n-символьными. Следовательно, мы можем определить каррирование как процесс взятия n-арной функции и превращения ее в n унарных функций. Давайте начнем с простого примера, функции, которая берет произведение точек двух векторов. Напомним из линейной алгебры, что скалярное произведение двух векторов [a, b, c] и [x, y, z] равно ax + by + cz.

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

console.log(dot(v1, v2)); // 1(4) + 3(-2) + (-5)(-1) = 4 - 6 + 5 = 3

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

function curriedDot(vector1) {
  return function(vector2) {
    return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
  }
}

// Taking the dot product of any vector with [1, 1, 1]
// is equivalent to summing up the elements of the other vector.
const sumElements = curriedDot([1, 1, 1]);

console.log(sumElements([1, 3, -5])); // -1
console.log(sumElements([4, -2, -1])); // 1

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

function dot(vector1, vector2) {
  return vector1.reduce((sum, element, index) => sum += element * vector2[index], 0);
}

const v1 = [1, 3, -5];
const v2 = [4, -2, -1];

// Use Ramda to do the currying for us!
const curriedDot = R.curry(dot);

const sumElements = curriedDot([1, 1, 1]);

console.log(sumElements(v1)); // -1
console.log(sumElements(v2)); // 1

// This works! You can still call the curried function with two arguments.
console.log(curriedDot(v1, v2)); // 3

И Ramda, и lodash также позволяют вам «пропустить» аргумент и указать его позже. Они делают это с помощью заполнителя. Поскольку скалярное произведение является коммутативным, не имеет значения, в каком порядке мы передали векторы в функцию. Давайте использовать другой пример, чтобы проиллюстрировать использование заполнителя. Ramda использует двойное подчеркивание в качестве заполнителя.

const giveMe3 = R.curry(function(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
});

const giveMe2 = giveMe3(R.__, R.__, 'French Hens');   // Specify the third argument.
const giveMe1 = giveMe2('Partridge in a Pear Tree');  // This will go in the first slot.
const result = giveMe1('Turtle Doves');               // Finally fill in the second argument.

console.log(result);
// 1: Partridge in a Pear Tree
// 2: Turtle Doves
// 3: French Hens

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

Язык JavaScript имеет встроенный механизм для частичного применения без каррирования. Это делается с помощью метода function.prototype.bind. Одна особенность этого метода в том, что он требует, чтобы вы указали значение this в качестве первого аргумента. Если вы не занимаетесь объектно-ориентированным программированием, вы можете игнорировать this, передавая значение null.

function giveMe3(item1, item2, item3) {
  return `
    1: ${item1}
    2: ${item2}
    3: ${item3}
  `;
}

const giveMe2 = giveMe3.bind(null, 'rock');
const giveMe1 = giveMe2.bind(null, 'paper');
const result = giveMe1('scissors');

console.log(result);
// 1: rock
// 2: paper
// 3: scissors

Завершение

Я надеюсь, вам понравилось изучать функциональное программирование на JavaScript вместе со мной! Для некоторых это может быть совершенно новая парадигма программирования, но я надеюсь, что вы дадите ей шанс. Я думаю, вы обнаружите, что ваши программы легче читать и отлаживать. Неизменность также позволит вам воспользоваться преимуществами оптимизации производительности в Angular и React.

Эта статья основана на выступлении Мэтта OpenWest, JavaScript the Good-er PartsOpenWest состоится 12-15 июля 2017 года в Солт-Лейк-Сити, штат Юта.

Оригинальная статья:  Matt BanzAn introduction to functional programming in JavaScript


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован.