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

Что это?

Функциональное программирование - это, в основном, программирование с помощью функций, и это вполне математическое.

На странице Википедии говорится:

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

Императивный и декларативный

Будучи программистом-самоучкой без степени CS, я никогда не думал и не изучал парадигмы программирования. Я знал только объектно-ориентированное программирование. Вот моя попытка описать их несколькими предложениями и примерами кода.

Императивное программирование. Последовательность команд, которые должен выполнять компьютер (как работает программа), важна. Обычно объектно-ориентированное программирование поддерживает императивное программирование. Обычно используются такие утверждения, как for, if и switch.

var nums = [1, 2, 3, 4, 5];
function powNums(numbers) {
    var powed = [];
    for(var i = 0, len = numbers.length; i < len; i++) {
        var newNum = numbers[i] * numbers[i];
        powed.push(newNum);
    }
    return powed;
}
console.log(powNums(nums)); // [1, 4, 9, 16, 25]

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

var nums = [1, 2, 3, 4, 5];
var powNums = function(numbers) {
    return numbers.map(function(n) {
        return n * n;
    });
};
console.log(powNums(nums)); // [1, 4, 9, 16, 25]

Функциональный язык

Функциональное программирование - это стиль, а язык функционального программирования - это язык, специально созданный для этого стиля. Javascript не является функциональным языком, но имеет функции для функционального программирования.

Функциональные языки - это такие вещи, как:

  • Haskell
  • Вяз
  • Закрытие

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

Что нужно знать / понимать

В этой статье я расскажу о следующих вещах:

  • Чистая функция
  • Неизменяемые данные
  • Функция первого класса и функция высшего порядка
  • Закрытие
  • map, filter, reduce
  • Каррирование
  • Состав функций
  • Рекурсивный
  • Мемоизация

Чистая функция

Поскольку уже есть очень хорошее объяснение, цитирую:

Чистая функция

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

В коде это будет следующим образом:

нечистый

var val = 0;
function increment() {
    val += 1; // val is changed
}

чистый

var val = 0;
function increment(x) {
    return x + 1; // val stays untouched. 
                  // only dealing with its inputs
}

Нечистая версия функции increment изменяется val прямо внутри тела, и здесь нет входов и выходов. Чистая версия напрямую не касается val. Вместо этого функция принимает входные данные и выводит значение, равное входному значению плюс 1.

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

Есть преимущества чистых функций.

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

Побочные эффекты… Это был для меня новый термин. Быстрый поиск в Google и Википедия говорят:

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

В основном адекватное руководство по функциональному программированию профессора Фрисби перечисляет следующие побочные эффекты:

  • изменение файловой системы
  • вставка записи в базу данных
  • сделать http-звонок
  • мутации
  • печать на экран / ведение журнала
  • получение пользовательского ввода
  • запрос DOM
  • доступ к состоянию системы

Устранить побочные эффекты невозможно. Вместо этого мы должны стремиться минимизировать их количество или максимально изолировать их.

Неизменяемые данные

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

Примечания:

  • const в javascript не создает неизменяемый объект. Сам объект все еще можно изменить.
  • Object.assign нельзя использовать для глубокого клонирования объектов.

В коде это выглядит так:

изменчивый

var nums = [1, 2, 3, 4];
nums.push(5); // impure function
console.log(nums); // [1, 2, 3, 4, 5]

неизменяемый

var nums = [1, 2, 3, 4];
var newNums = nums.concat([5]); // pure function
console.log(nums);    // [1, 2, 3, 4]
console.log(newNums); // [1, 2, 3, 4, 5]

переполнение стека - что такое состояние гонки?
переполнение стека - что такое тупик?

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

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

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

// assigning a function to a variable
var aFunc = function(x, f) {
    return f(x);
};
// passing a function as an argument
var val = aFunc(0, increment);
console.log(val); // 1

Закрытие

Закрытие

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

var funcWithClosure = function() {
    var v = 3; // a local variable created by funcWithClosure
    
    return function(x) { // inner function keeps a reference
                         // to its lexical environment
        return x + v;    // and can access the variables
                         // of outer functions
    };
};
var closureFunc = funcWithClosure();
var val = closureFunc(2);
console.log(val); // 5

Пока что все говорит само за себя. Первым этапом, когда эти концепции стали применимыми и функциональными, я понял и использовал функции map, filter и reduce массива Javascript.

карта, фильтр, уменьшить

В массиве Javascript есть функции map, filter и reduce, которые удобны для создания нового значения из массива без изменений. Вот краткое объяснение каждой функции словами и кодами.

карта

Array.prototype.map создает и возвращает новый массив с результатами вызова предоставленной функции для каждого элемента в массиве.

var nums = [1, 2, 3, 4, 5];
var timesTwo = function(x) { // pure function
    return x * 2;
};
var newNums = nums.map(timesTwo); // timesTwo function is passed as
                                  // argument and applied to each
                                  // element
console.log(nums);    // [1, 2, 3, 4, 5]
console.log(newNums); // [2, 4, 6, 8, 10]

фильтр

Array.prototype.filter создает и возвращает новый массив со всеми элементами, прошедшими указанную функцию тестирования.

var nums = [1, 2, 3, 4, 5];
var isEven = function(x) { // pure function
    return x % 2 === 0;
};
var newNums = nums.filter(isEven); // isEven function is passed as 
                                   // argument and applied to each 
                                   // element
console.log(nums);    // [1, 2, 3, 4, 5]
console.log(newNums); // [2, 4]

уменьшить

Array.prototype.reduce немного отличается от map и filter. Он возвращает одно значение, накопленное из каждого элемента в массиве (слева направо), переданном в предоставленную функцию.

var nums = [1, 2, 3, 4, 5];
var sum = function(accum, x) { // pure function
    return accum + x;
};
var sumOfNums = nums.reduce(sum); // sum function is passed as
                                  // argument and applied to each
                                  // element
console.log(nums);      // [1, 2, 3, 4, 5]
console.log(sumOfNums); // 15

Начальное значение накопления может быть дополнительно передано в reduce в качестве начального значения. Если начальное значение не указано (как в примере выше), используется первый элемент массива.

var nums = [1, 2, 3, 4, 5];
var sum = function(accum, x) {
    return accum + x;
};
var sumOfNums = nums.reduce(sum, 10); // set the initial value to 10 
console.log(nums);      // [1, 2, 3, 4, 5]
console.log(sumOfNums); // 25

в цепочку

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

var newNums = nums
                .filter(isEven)
                .map(timesTwo)
                .map(timesTwo);
console.log(nums);    // [1, 2, 3, 4, 5]
console.log(newNums); // [8, 16]

Выше то же самое, что и:

var newNums1 = nums.filter(isEven);
var newNums2 = newNums1.map(timesTwo);
var newNums3 = newNums2.map(timesTwo);
console.log(newNums3); // [8, 16]

А как насчет карты, фильтрации, уменьшения с помощью объектов?

Хотя только массив имеет функции map, filter и reduce, можно реализовать те же функции для объектов, используя reduce.

карта (объект)

var nums = { a: 1, b: 2, c: 3, d: 4, e: 5 };
var newNums = Object.keys(nums).reduce(function(result, key) { 
    result[key] = nums[key] * 2;
    return result;
}, {}); // providing an empty object as initial value 
console.log(nums);    // { a: 1, b: 2, c: 3, d: 4, e: 5 } 
console.log(newNums); // { a: 2, b: 4, c: 6, d: 8, e: 10 }

фильтр (объект)

var nums = { a: 1, b: 2, c: 3, d: 4, e: 5 };
var newNums = Object.keys(nums).reduce(function(result, key) { 
    if(nums[key] % 2 === 0) {
        result[key] = nums[key];
    }
    return result;
}, {});
console.log(nums);    // { a: 1, b: 2, c: 3, d: 4, e: 5 } 
console.log(newNums); // { b: 2, d: 4 }

уменьшить (объект)

var nums = { a: 1, b: 2, c: 3, d: 4, e: 5 };
var sumOfNums = Object.keys(nums).reduce(function(result, key) { 
    return result + nums[key];
}, 0);
console.log(nums);      // { a: 1, b: 2, c: 3, d: 4, e: 5 } 
console.log(sumOfNums); // 15

НЕ соединяется в цепочку

Поскольку приведенные выше примеры каждый раз возвращают новый объект, и мы должны сначала вызвать Object.keys, чтобы использовать Array.prototype.reduce, невозможно связать функции, как мы видели с массивом.

Чтобы объединить их в цепочку, нам нужно изучить такие вещи, как «каррирование» и «композиция функций».

Но я хотел бы сначала сделать обход стрелочных функций, которые были введены в ES6, и сначала «Сигнатура типа».

Стрелочные функции

Выражение стрелочной функции - одно из нововведений в ES6. Это позволяет использовать более короткий синтаксис для определения функции. И он не связывает свои собственные this или arguments, и его нельзя использовать в качестве конструктора. Более подробное объяснение можно найти на этой странице на MDN.

функциональное выражение (мы уже знаем)

var aFunc = function(x) {
    return x * 2;
};

стрелочная функция

const aFunc = (x) => {
    return x * 2;
};
// if the argument is only one, parentheses can be omitted
const aFunc = x => {
    return x * 2;
};
// if the body contains only one line,
// curly brackets can be omitted.
// and return is implicitly attached
const aFunc = x => x * 2;

Типовые подписи

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

В Haskell:

functionName :: arg1Type -> arg2Type ->...-> argNType -> outputType

Или в вяз:

functionName : arg1Type -> arg2Type ->...-> argNType -> outputType

Они являются частью этих языков и помогают компиляторам проверять типы.

Однако этот синтаксис недействителен в Javascript, поэтому мы можем использовать comment для его имитации.

// aFunc :: Number -> Number
const aFunc = x => x * 2;
// getStringLength :: String -> Int
const getStringLength = s => s.length;
// arrayToString :: [Char] -> String
const arrayToString = a => a.join('');
// filterNumberArray :: (Number -> Bool) -> [Number] -> [Number]
const filterNumberArray = (f, a) => a.filter(f);

Эта страница Ramda.js имеет подробные пояснения.

Это конец объезда. Теперь перейдем к «каррированию» и «композиции функций».

Каррирование

Каррирование

… означает декомпозицию функции, которая принимает несколько аргументов, в функцию, которая принимает первый аргумент и возвращает функцию, которая принимает следующий аргумент, и так далее для всех аргументов. ( Практическое введение в функциональное программирование от Мэри Роуз Кук )

Чтобы показать, что это за код:

нормальный

// normalFunc :: Number -> Number -> Number
const normalFunc = (x, y) => x * y;
// if it's confusing, below is the same
var normalFunc = function(x, y) {
    return x * y;
}

карри

// curriedFunc :: Number -> (Number -> Number)
const curriedFunc = x => y => x * y;
// if it's confusing, below is the same
var curriedFunc = function(x) {
    return function(y) {
        return x * y;
    };
};
// this is possible thanks to closures

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

нормальный

const val1 = normalFunc(2, 4); // 8

карри

const timesTwo = curriedFunc(2);
const val2 = timesTwo(4); // 8

Как это могло быть полезно?

// times :: Number -> (Number -> Number)
const times = x => y => x * y;
// creating new functions based on times function
const timesTwo   = times(2);
const timesThree = times(3);
// mapArray :: (Number -> Number) -> ([Number] -> [Number])
// this function takes a function that takes a number and 
// outputs a number and returns another function that takes
// an array of numbers and returns another array of numbers
const mapArray = f => a => a.map(f); 
// passing timesTwo and timesThree functions to
// mapArray function to create new functions for array
const mapArrayByTimesTwo   = mapArray(timesTwo);
const mapArrayByTimesThree = mapArray(timesThree);
const a = [1, 2, 3, 4, 5];
const a2  = mapArrayByTimesTwo(a);
const a3  = mapArrayByTimesThree(a2);
const a23 = mapArrayByTimesThree(mapArrayByTimesTwo(a)); 
console.log(a);   // [1, 2, 3, 4, 5]
console.log(a2);  // [2, 4, 6, 8, 10]
console.log(a3);  // [6, 12, 18, 24, 30]
console.log(a23); // [6, 12, 18, 24, 30]

Несмотря на то, что возможность создавать новые функции на основе функции является хорошей особенностью, указанная выше функция times не может быть вызвана как times(2, 5) с текущей реализацией. Он должен называться times(2)(5). Это не кажется таким полезным, особенно когда функция принимает больше аргументов. По сравнению с aFunc(a, b, c, d), aFunc(a)(b)(c)(d) сложнее с первого взгляда понять, что происходит, требует большего набора текста и выглядит некрасиво. Люди должны жить в хорошем стиле и со здоровыми эстетическими ценностями.

Кроме того, mapArrayByTimesThree(mapArrayByTimesTwo(a)) может быть дорогостоящим вызовом, если массив a имеет много элементов, поскольку mapArrayByTimesThree принимает массив, созданный mapArrayByTimesTwo, что приводит к созданию нового массива дважды. Это также можно сказать о цепочке Array.prototype.map или Array.prototype.filter. Было бы лучше, если бы функция генерировалась из серии функций и вызывалась только один раз, когда она передается в map или filter функции.

Состав функций

Когда функция f принимает вход a и выдает b, а другая функция g принимает вход b и выдает c, новая функция fg может состоять из этих двух функций, принимающих вход a и выход c.

Функция f и g: a -> b -> c
Функция fg: a -> c

Если выход функции A и вход B одного типа, указанная выше логика может быть применена к программированию.

// compose (Any -> Any) -> (Any -> Any) -> (Any -> Any)
// this function takes two functions as argument
// and returns a function
const compose = (g, f) => x => g(f(x));
// can also be written as
var compose = function(g, f) {
    return function(x) {
        return g(f(x));
    };
};

timesTwo и timesThree из предыдущего раздела принимают Number тип и возвращают Number тип. Таким образом, их можно составить в timesSix:

const timesSix = compose(timesThree, timesTwo);

Примечание. Аргументы compose выполняются справа налево.

mapArrayByTimesThree(mapArrayByTimesTwo(a)) можно переписать как:

// times :: Number -> (Number -> Number)
const times = x => y => x * y;
// mapArray :: (Number -> Number) -> ([Number] -> [Number])
const mapArray = f => a => a.map(f);
// creating new functions based on times function defined above 
const timesTwo   = times(2);
const timesThree = times(3);
const timesSix = compose(timesThree, timesTwo);
const mapArrayByTimesSix = mapArray(timesSix);
const a23 = mapArrayByTimesSix(a);
console.log(a);   // [1, 2, 3, 4, 5]
console.log(a23); // [6, 12, 18, 24, 30]

Теперь новый массив создается только один раз.

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

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

Для этого мы можем использовать Array.prototype.reduceRight, который обращается к элементам справа налево (Array.prototype.reduce обращается к элементам в массиве слева направо).

const compose = (...fs) => 
    x => fs.reduceRight((v, f) => f(v), x);

Если использовать Array.prototype.reduce вместо Array.prototype.reduceRight, мы можем создать так называемую функцию pipe. pipe выполняет переданные функции слева направо.

const pipe = (...fs) =>
    x => fs.reduce((v, f) => f(v), x);

Рекурсивный

aFunc(a)(b)(c)(d) все еще не решен. Нам нужно создать curry функцию, которая принимает функцию в качестве аргумента и возвращает новую функцию, которая продолжает возвращать функции до тех пор, пока не будут переданы все необходимые аргументы и исходная функция не будет выполнена с необходимыми аргументами.

Нравится:

aFunc(a, b, c, d);
aFunc(a)(b, c, d);
aFunc(a)(b)(c, d);
aFunc(a, b)(c, d);
// ...
aFunc(a)(b)(c)(d);

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

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

Вот пример:

// factorial :: Number -> Number
const factorial = n => 
    n < 2 ? 1 : n * factorial(n - 1); // calling itself
// or
var factorial = function(n) {
    return n < 2 ? 1 : n * factorial(n - 1); // calling itself
};
console.log(factorial(10)); // 3628800

Если бы это было с for циклом, это можно было бы записать как:

var factorial = function(n) { // this is not recursive
    var result = 1;
    
    for (var i = 1; i <= n; i++) {
        result = i * result;
    }
    
    return result;
};

Но у рекурсивной factorial функции есть предел. Если передать ему большое число:

console.log(factorial(32768));
// Uncaught RangeError: Maximum call stack size exceeded

Есть способы решить эту проблему.

Как правило, языки функционального программирования имеют функцию «оптимизации хвостового вызова» для рекурсивных функций. Однако javascript не поддерживает эту функцию (это и да, и нет). Что происходит, так это то, что каждый раз, когда функция вызывается, в стеке вызовов создается новый кадр стека для аргументов и локальных значений. Если функция повторяется много раз, интерпретатору или компилятору не хватает памяти. Языки с оптимизацией хвостового вызова повторно используют один и тот же кадр стека для всей последовательности рекурсивных вызовов если вызов функции происходит как последнее действие другой функции.

В приведенной выше функции factorial необходимо изменить часть с n * factorial(n - 1), поскольку последнее действие функции умножает возвращаемое значение factorial(n - 1) на n.

Вместо этого в следующий вызов можно передать ранее умноженное значение.

Оптимизированная версия хвостового вызова:

const factorial = n => {
    const _factorial = (n, accum) => 
        n < 2 ? accum : _factorial(n - 1, n * accum); // calling a function as the last action
    return _factorial(n, 1);
};
factorial(10); // 3628800
// _factorial 10, 1
// _factorial  9, 10
// _factorial  8, 90
// _factorial  7, 720
// _factorial  6, 5040
// _factorial  5, 30240
// _factorial  4, 151200
// _factorial  3, 604800
// _factorial  2, 1814400
// _factorial  1, 3628800
// 3628800

Примечание. Несмотря на то, что в ES6 есть оптимизация хвостовых вызовов, в node.js и браузерах без поддержки ES6 все еще есть проблема с размером стека даже с новой функцией factorial, описанной выше (есть способы исправить это с помощью таких методов, как trampolining, и комбинатор с фиксированной точкой).

С рекурсивной функцией функция curry теперь может быть записана как:

const curry = f => {
    const arity = f.length; // arity means the number of arguments 
    
    const _f = (...args) =>
        (...a) => {
            const _a = [...args, ...a];
            return _a.length >= arity ? f(..._a) : _f(..._a);
        };
    
    return _f();
};
const times = curry((x, y) => x * y);
const timesTwo = times(2);
console.log(timesTwo(4)); // 8
console.log(times(2)(4)); // 8
console.log(times(2, 4)); // 8

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

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

const partial = (f, ...args) => (..._args) => f(...args, ..._args);

Вернуться к карте и фильтровать по объекту

Теперь с функцией curry, map функцию для объекта можно переписать как:

const oMap = curry((f, obj) =>
    Object.keys(obj).reduce((result, key) => {
        result[key] = f(key, obj[key]);
        return result;
    }, {}));
const timesTwo = (key, value) => value * 2;
const nums = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const newNums = oMap(timesTwo, nums);
console.log(nums);    // { a: 1, b: 2, c: 3, d: 4, e: 5 }
console.log(newNums); // { a: 2, b: 4, c: 6, d: 8, e: 10 }

Используя compose, oMap можно связать как:

const timesFour = compose(oMap(timesTwo), oMap(timesTwo));
console.log(timesFour(nums)); // { a: 4, b: 8, c: 12, d: 16, e: 20 }

Выше эквивалентно:

console.log(oMap(timesTwo, oMap(timesTwo, nums)));

То же самое и с filter:

const oFilter = curry((f, obj) => 
    Object.keys(obj).reduce((result, key) => {
        if(f(key, obj[key])) {
            result[key] = obj[key];
        }
        return result;
    }, {}));
const isEven = (key, value) => value % 2 === 0;
const nums = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const newNums = oFilter(isEven, nums);
console.log(nums);    // { a: 1, b: 2, c: 3, d: 4, e: 5 }
console.log(newNums); // { b: 2, d: 4 }

Объединяя их как:

const gteTen = (key, value) => value >= 10;
const newFunc = compose( oFilter(gteTen)
                       , oMap(timesTwo)
                       , oMap(timesTwo)
                       );
console.log(newFunc(nums)); // { c: 12, d: 16, e: 20 }

Воспоминание (наконец, хотя это еще не конец)

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

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

memoize функцию можно было бы записать как:

const memoize = f => {
    let cache = {};
    return (...arg) => {
        const arg_str = JSON.stringify(arg);
        // check if the output value is in the cache
        cache[arg_str] = cache[arg_str] || f(...arg);
        return cache[arg_str];
    }
};
const times = memoize((x, y) => x * y);
console.log(times(2, 3)); // 6
console.log(times(2, 3)); // 6 (cached)
console.log(times(2, 3)); // 6 (cached)

Спасибо, что дочитали до этого места. На этом «Функциональное программирование в Javascript, часть 1» заканчивается. Я надеюсь, что тот, кто прочитает это, найдет одну или две полезные вещи.

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

И, наконец, я настоятельно рекомендую всем, кто интересуется, прочитать эти два руководства:

Практическое введение в функциональное программирование от Мэри Роуз Кук

Практически адекватное руководство профессора Фрисби по функциональному программированию