Понять Композицию в 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
Простая аналогия: Приготовление бутерброда
Давайте соотнесём композицию функций с приготовлением бутерброда — реальным примером, с которым знакомы многие из нас.
- Функция нарезки хлеба: Начните с нарезки хлеба.
- Функция намазывания: Далее наносится масло, майонез или любое другое средство по вашему выбору.
- Функция наполнения: Наконец, добавляется начинка — салат, помидор, сыр и т.д.
Каждый этап приготовления бутерброда может быть представлен в виде функции. При объединении этих функций получается композитная функция — процесс приготовления бутерброда!
Разбиение на части с помощью 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
без явного указания на то, как данные перемещаются между этими функциями.
Зачем использовать Бесточечный
- Лаконичность: Уменьшает многословность, концентрируя внимание на операциях, а не на данных.
- Читаемость: В коде акцентируется внимание на процессе преобразования, что делает его более декларативным.
- Возможность многократного использования: Абстрагирование от специфических аргументов приводит к созданию более общих и многократно используемых функций.
Главное, что можно понять, — это то, что бесточечный стиль заключается в создании функций, в которых поток данных является неявным. Хотя такие библиотеки, как 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. Наследование
Спор вокруг композиции и наследования не прекращается. Если наследование — это расширение свойств и поведения базовой сущности, то композиция функций — это объединение простых функций для создания более сложных, что способствует модульности и возможности повторного использования.
- Гибкость: Композиция обеспечивает большую гибкость, поскольку позволяет легко подключать и отключать функциональные возможности. Наследование может привести к созданию жёсткой структуры, которую сложно модифицировать по мере развития приложения.
- Отношения: Наследование устанавливает отношения
is-a
(GrilledCheeseSandwich is a Sandwich), в то время как композиция функций подразумевает отношенияhas-a
илиuses-a
, указывающие на то, как компоненты связаны или используются. - Возможность повторного использования: Хотя обе концепции способствуют многократному использованию, композиция функций делает это без привязки кода к конкретной объектной структуре, что делает его более адаптируемым и удобным для понимания.
Смешение концепций
В реальных приложениях можно встретить сценарии, в которых оптимальным решением является сочетание наследования и композиции функций. Тщательно оценив требования и поняв сильные и слабые стороны обеих парадигм, можно гармонично использовать их для создания надёжных, сопровождаемых и эффективных приложений.
Хотя композиция функций и наследование могут показаться двумя сторонами одной медали, они служат разным целям и используются в разных контекстах. Понимание того, когда и как использовать каждую из них, позволит вам писать более универсальный и эффективный код.
Последний кусочек
Композиция функций — это мощная концепция JavaScript, позволяющая создавать модульный, многократно используемый и поддерживаемый код. Понимая эту технику, вы не просто делаете бутерброды, а создаёте универсальную кухню, на которой можно готовить самые разнообразные блюда!
Помните, что главное — это практика. Чем больше вы экспериментируете с сочетанием различных функций, тем более комфортно вы будете работать с композицией функций. Итак, вперёд, экспериментируйте с рецептами и создавайте деликатесы кодирования!