Понять Композицию в JavaScript раз и навсегда

Источник: «Understand JavaScript Composition Once and for All»
Когда мы вступаем на путь кодирования на JavaScript, мы сталкиваемся с множеством концепций, и одной из таких интересных концепций является Композиция Функций.

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

Что такое Композиция функций

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

Математически, если даны две функции f и g, то их композиция представляется как f(g(x)). Здесь сначала вычисляется g(x), а её результат передаётся в f.

const f = x => x + 2;
const g = x => x * 3;

// Композиция f и g
const composedFunction = x => f(g(x)); // f(g(x)) = f(3x) = 3x + 2

console.log(composedFunction(2)); // Вывод 8

Простая аналогия: Приготовление бутерброда

Давайте соотнесём композицию функций с приготовлением бутерброда — реальным примером, с которым знакомы многие из нас.

  1. Функция нарезки хлеба: Начните с нарезки хлеба.
  2. Функция намазывания: Далее наносится масло, майонез или любое другое средство по вашему выбору.
  3. Функция наполнения: Наконец, добавляется начинка — салат, помидор, сыр и т.д.

Каждый этап приготовления бутерброда может быть представлен в виде функции. При объединении этих функций получается композитная функция — процесс приготовления бутерброда!

Разбиение на части с помощью JavaScript

Давайте превратим нашу аналогию с бутербродом в функции JavaScript!

// Функция нарезки хлеба
const sliceBread = bread => `${bread} is sliced`;

// Функция намазывания
const spreadButter = bread => `Butter spread on ${bread}`;

// Функция наполнения
const addFilling = bread => `Filling added to ${bread}`;

// Композиция функций для создания бутерброда
const makeSandwich = bread => addFilling(spreadButter(sliceBread(bread)));

console.log(makeSandwich("Whole Wheat"));
// Вывод: "Filling added to Butter spread on Whole Wheat is sliced"

Многоразовость и сопровождаемость

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

const makeToast = bread => spreadButter(sliceBread(bread));
console.log(makeToast("White Bread"));
// Вывод: "Butter spread on White Bread is sliced"

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

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

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

// Композиция двух функций
const compose = (f, g) => x => f(g(x));

const makeSandwich = compose(addFilling, compose(spreadButter, sliceBread));

console.log(makeSandwich("Rye Bread"));
// Вывод: "Filling added to Butter spread on Rye Bread is sliced"

Замыкание: Плавная композиция функций

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

const addTopping = topping => bread => `${topping} added to ${bread}`;
const addLettuce = addTopping("Lettuce");
console.log(addLettuce("Butter spread on Whole Wheat is sliced"));
// Вывод: "Lettuce added to Butter spread on Whole Wheat is sliced"

Вот что я написал в 2017 году, когда разобрался с практическим применением Каррирования. Возможно, это поможет и вам.

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

Библиотеки профессиональной композиции

Хотя понимание и реализация базовой композиции функций очень важны, в профессиональной среде разработчики часто используют такие библиотеки, как Ramda или Lodash/fp. Эти библиотеки предлагают набор полезных функций, которые делают функциональное программирование и композицию функций на JavaScript более доступными и удобными.

Пример Ramda:

import R from 'ramda';

const sliceBread = bread => `${bread} is sliced`;
const spreadButter = bread => `Butter spread on ${bread}`;
const addFilling = bread => `Filling added to ${bread}`;

// Использование функции композиции Ramda
const makeSandwich = R.compose(addFilling, spreadButter, sliceBread);

console.log(makeSandwich("Sourdough"));
// Вывод: "Filling added to Butter spread on Sourdough is sliced"

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

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

Вот как можно определить функцию compose в TypeScript:

function compose<A, B, C>(f: (b: B) => C, g: (a: A) => B): (a: A) => C {
return (x: A) => f(g(x));
}

const sliceBread = (bread: string) => `${bread} is sliced`;
const spreadButter = (bread: string) => `Butter spread on ${bread}`;
const addFilling = (bread: string) => `Filling added to ${bread}`;

const makeSandwich = compose(addFilling, compose(spreadButter, sliceBread));

console.log(makeSandwich("Multigrain"));
// Вывод: "Filling added to Butter spread on Multigrain is sliced"

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

Монады и композиция Функторов

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

Монады и Функторы могут быть составлены так же, как и обычные функции, и являются основой для таких продвинутых паттернов функционального программирования, как монада Maybe и монада Either. Изучение этих паттернов позволяет получить мощные инструменты для изящной обработки ошибок и написания более надёжного и удобного кода.

Поскольку это может быть очень глубоко, я оставлю это для другой статьи. Даю слово.

Бесточечный стиль (комбинаторное программирование)

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

До Бесточечного:

Приведём простой пример без использования бесточечного стиля:

const double = x => x * 2;
const increment = x => x + 1;

const transform = x => increment(double(x));

После Бесточечного:

Теперь о принятии бесточечного стиля в чистом JavaScript:

const double = x => x * 2;
const increment = x => x + 1;

// Это базовая функция композиции
const compose = (f, g) => x => f(g(x));

const transform = compose(increment, double);

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

Зачем использовать Бесточечный

  1. Лаконичность: Уменьшает многословность, концентрируя внимание на операциях, а не на данных.
  2. Читаемость: В коде акцентируется внимание на процессе преобразования, что делает его более декларативным.
  3. Возможность многократного использования: Абстрагирование от специфических аргументов приводит к созданию более общих и многократно используемых функций.

Главное, что можно понять, — это то, что бесточечный стиль заключается в создании функций, в которых поток данных является неявным. Хотя такие библиотеки, как Ramda, делают этот стиль более доступным, его вполне можно применять и на обычном JavaScript.

А что с Наследованием

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

Наследование — это принцип объектно-ориентированного программирования (ООП), позволяющий одному классу наследовать свойства и методы другого класса. Он способствует многократному использованию кода и устанавливает связь между базовым (родительским) классом и производным (дочерним) классом.

class Sandwich {
constructor(bread) {
this.bread = bread;
}

make() {
return `${this.bread} sandwich is made`;
}
}

class GrilledCheeseSandwich extends Sandwich {
make() {
return `Grilled Cheese ${super.make()}`;
}
}

const grilledCheese = new GrilledCheeseSandwich("White Bread");
console.log(grilledCheese.make()); // Вывод: "Grilled Cheese White Bread sandwich is made"

В приведённом примере GrilledCheeseSandwich является дочерним классом, наследующим от родительского класса Sandwich. Он переопределяет метод make для настройки процесса приготовления сэндвича/бутерброда.

Композиция vs. Наследование

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

  1. Гибкость: Композиция обеспечивает большую гибкость, поскольку позволяет легко подключать и отключать функциональные возможности. Наследование может привести к созданию жёсткой структуры, которую сложно модифицировать по мере развития приложения.
  2. Отношения: Наследование устанавливает отношения is-a (GrilledCheeseSandwich is a Sandwich), в то время как композиция функций подразумевает отношения has-a или uses-a, указывающие на то, как компоненты связаны или используются.
  3. Возможность повторного использования: Хотя обе концепции способствуют многократному использованию, композиция функций делает это без привязки кода к конкретной объектной структуре, что делает его более адаптируемым и удобным для понимания.

Смешение концепций

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

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

Последний кусочек

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

Помните, что главное — это практика. Чем больше вы экспериментируете с сочетанием различных функций, тем более комфортно вы будете работать с композицией функций. Итак, вперёд, экспериментируйте с рецептами и создавайте деликатесы кодирования!

Дополнительные материалы

Предыдущая Статья

Заполнение пропусков в результатах статистических временных рядов

Следующая Статья

Валидация JSON Schema для столбцов