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

Функциональное программирование(FP) — способ организации кода через написание набора функций.

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

Отсюда следуют следующие определения:

Функциональный агрумент — аргумент, значением которого является функция.

Функция высшего порядка — функция, которая принимает функции в качестве аргументов.

Функции с функциональным значением — функция, которая возвращает функцию.

Давайте на примере посмотрим что к чему.

// 👇 - triple – это функция высшего порядка, потому что она получает другую функцию как аргумент или возвращает функцию как результат
// | 👇 - а это функциональный аргумент 'func' функции 'triple'
function triple(func){
return () => {
return [
func()
func()
func()
]
}
}
function bar(){
console.log("bar")
}
// 👇 здесь мы вызываем функцию 'triple' и передаем в нее функцию 'bar' как аргумент
const triple_bar = triple(bar)

Мы же знаем, что функция triple вернет другую функцию, которую мы сохранили в переменную triple_bar. А значит что теперь мы можем функцию triple_bar вызвать:

triple_bar();

Рекурсия

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

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

  • Терминальное условие — условие прекращения выполнения
  • Правило по которому осуществляется движение в глубь по рекурсии

Рассмотрим самый популярный пример рекурсии — вычисление факториала.

./recursion.js
const factorial = (n) => {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
};

Выделим характерные составляющие рекурсивной функции. Терминальное условие

const factorial = (n) => {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
};

и правило движения по рекурсии

const factorial = (n) => {
if (n === 0) {
return 1;
}
return n * factorial(n - 1);
};

Рекурсия как замена циклу

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

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

Давайте не будем мусолить и посмотрим на замену следующего цикла c помощью рекурсии. Вот сам цикл:

var i = 0;
while (i < 3) {
console.log(i);
i++;
}

Что мы здесь видим? Ну то, что "терминальное условие цикла" - это i >= 3. Значит мы можем легко переписать пример через рекурсию:

const iteration = (acc) => {
if (acc >= 3) return;
console.log(acc);
return iteration(acc + 1);
};
iteration(0);

Вуаля👏👏👏! Если вы прогоните этот код, то результат и там и там будет аналогичным.

Обход дерева

Рекурсию удобно использовать для работы с рекурсивными структурами данных, такими как вложенные списки или деревья.

note

Обратите внимание на определение рекурсии, для того чтобы стало понятнее что такое рекурсивные структуры данных.

Представим следующее дерево:

const FOLDER = "folder";
const FILE = "file";
const FS_TREE = {
type: FOLDER,
name: "MyDocuments",
children: [
{
type: FOLDER,
name: "Videos",
children: [
{
type: FILE,
name: "new_year.pdf",
},
{
type: FILE,
name: "my_dr.pdf",
},
],
},
{
type: FILE,
name: "cv.pdf",
},
],
};
Да что это за деревья!😡⁉️

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

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

const getFilesFromFolder = (node) => {
if (node.type !== FOLDER) {
throw new Error("Бро! Ты можешь передать сюда только папку");
}
return node.children.filter((node) => {
if (node.type === FILE) {
return true;
}
});
};

Что мы сейчас получим если применим функцию getFilesFromFolder к папке в переменной FS_TREE?

getFilesFromFolder(FS_TREE);

В ответ мы получим примерно такой объект:

{
type: "file",
name: "cv.pdf"
}

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

const getFilesFromFolder = (node) => {
const result = []; // массив в который мы будем добавлять найденные файлы
if (node.type !== FOLDER) {
throw new Error("Бро! Ты можешь передать сюда только папку");
}
node.children.forEach((item) => {
// проверяем тип узла
switch (item.type) {
case FILE:
// если это file. то добавляем его в массив result
result.push(item);
case FOLDER:
// если это folder. то вызываем getFilesFromFolder и файлы которые получили из этой функци добавляем в result
reslut.concat(getFilesFromFolder(item));
default:
throw new Error("Неизвестный тип узла");
}
});
};

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

Напоминаю, что функции высшего порядка это те функции которые получают другие функции как свои аргумент или возвращают функции. На постоянной основе вы будете пользоваться HOF-методами массивов: Array.prototype.map(), Array.prototype.filter(), Array.prototype.find(), Array.prototype.forEach() и Array.prototype.reduce().

Array.prototype.map()

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

Давайте посмотрим на достаточно простой пример, у нас есть массив чисел:

const NUMS = [1, 2, 3, 4];

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

const x2 = (i) => i * 2;

и применем ее к каждому элементу в NUMS:

console.log(NUMS.map(x2)); // [2,4,6,8]

Array.prototype.filter()

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

const NUMS = [20, 400, 300, 150, 50, 60];
const more_then_100 = [];
for (const i of NUMS) {
if (i > 100) {
more_then_100.push(i);
}
}
console.log(more_then_100); // [400, 300, 150]

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

const isMoreThen100 = (n) => n > 100;

После написания фунции мы можем передать ее в метод filter:

const more_then_100 = NUMS.filter(isMoreThen100);
console.log(more_then_100); // [400, 300, 150]