Функциональные штуки . Композиция
Функциональное
Про функциональный подход говорят часто и много. Кто-то считает, что это бесполезная ерунда, кто-то считает, что есть особая секта поклонников функционального программирования с тайной символикой и паролями вида "монада — это моноид в категории эндофункторов". Начинающим фронтятам говорят: чтобы пройти собес, нужно знать про чистые функции ☝️. К сожалению, им не всегда объясняют, зачем они нужны. Еще очень любят рассказывать про различие в императивном и декларативном подходах. Я, кстати, тоже так делаю :)
Давайте отбросим все эти несомненно полезные знания и попробуем посмотреть на функциональные штуки с другой стороны. А именно — немножко разберемся в очень изящной теории, которая драйвит весь функциональный подход.
Люди и код
Код пишут люди :) У людей весьма ограничена память, поэтому (большинство) писателей кода не создают огромные модули на тысячу строк. Чтобы как-то управлять сложностью, нам нужно разбивать написанное на кусочки и удерживать их в голове. Также нам нужен какой-то способ строить из кусочков конструкции посложнее. Тогда мы можем забыть о внутренней структуре кусочка и сосредоточиться на форме нашей конструкции. Если придерживаться определенных правил композиции кусочков, мы сможем получить крутые спецэффекты и интересные конструкции. Посему переходим к композиции.
Категории и Композиция
Пусть у нас есть набор объектов. Пусть эти объекты каким-то образом связаны друг с другом. В теории категорий эти связи называют морфизмами (или стрелками).
Давайте на примере. Пусть у нас есть свинка.
Свинка? Идея утащена из книжки «Category Theory For Programmers»
Есть замечательная книжка где автор (Bartosz Milewski) объясняет теорию категорий на свинках и стрелках. Я насколько вдохновилась этой книжкой, что решила для иллюстраций тоже использовать хрюшечек :)
Повернем свинку.
А теперь проделаем это несколько раз.
У нас получилась категория со свинками, морфизмами в которой являются углы поворота хрюш.
Эти стрелочки мне что-то напоминают 🤔
Возможно, вы обратили внимание, что последняя картинка напоминает граф. И это действительно граф с несколькими дополнительными операциями.
Чтобы категория действительно была категорией, нам нужно, чтобы выполнялось 4 условия.
1. Морфизмы можно композить
Более строго: Для любых двух морфизмов
должен существовать третий – их композиция
Мы обозначаем композицию вот так:
Какой-то порядок странный у этой вашей композиции
Сначала мне казалось зашкварным, что мы сначала пишем g, а потом f. Но давайте добавим аргумент к каждой функции. Кажется, теперь все встало на свои места :)
2. Композиция морфизмов ассоциативна
Демонстрирую на свинках
Давайте проверим, что для свинок это выполняется. Если мы повернем свинку на 120 = (30 + 90) градусов, потом на 60, то получится свинка вверх тормашками.
То же самое получится, если мы сначала повернем свинку на 90, потом еще на 90.
Мы предполагаем что все изображения свинок идентичны и каждый поворот происходит вокруг центра картинки (свинка крутится вокруг своего 🐽).
В нашей категории композиция — это просто сложение углов поворота.
3. В нашей категории есть тождественные морфизмы (identity), которые отображают объекты сами в себя
Тождественный морфизм специфичен для каждого объекта в категории. Чтобы это явно обозначить, мы добавляем объект, на который действует морфизм в качестве индекса.
Угадайте, что является тождественным морфизмом в категории со свинками
Поворот на 360°, 2 * 360°, 3 * 360° и т.д.
4. Тождественный морфизм не влияет на получившийся результат
В нашем примере все четыре условия выполняются. Мы получили хрюшекатегорию :)
Еще один пример (откровенно говоря, притянутый за уши :)
Думаете, что крутящиеся поросята не очень помогут в разработке? Окей, давайте по-другому. Каждый мечтает сделать свой маленький redux. Давайте попробуем
const increment = state => state + 3
const decrement = state => state - 3
const identity = state => state
Получаем категорию, где объекты — это значения state, морфизм – oдна из функций выше. Композиция определяется функцией compose.
const compose = (...fns) => fns.reduce((f, g) => x => f(g(x)), identity)
Теперь давайте добавим оптимизацию. Мы не хотим трогать state, если результат не изменился. Поэтому для вот такой последовательности increment -> increment -> increment -> decrement -> decrement -> decrement мы вообще не хотим ничего вызывать.
Воспользуемся тем, что identity не меняет результат (4.) и ассоциативностью композиции (2.)
const compact = fns => {
const prev = []
let i = 0
for (const f of fns) {
i++
if (f === identity) {
// identity не афектит
return compact(prev.concat(fns.slice(i)))
}
const prevElement = prev[prev.length - 1]
// мы можем композить в любом месте. Не обязательно вначале или в конце массива
if (
(prevElement === decrement && f === increment) ||
(prevElement === increment && f === decrement)
) {
prev.pop()
prev.push(identity)
return compact(prev.concat(fns.slice(i)))
} else {
prev.push(f)
}
}
return prev
}
const compose = (...fns) =>
compact(fns).reduce((f, g) => x => f(g(x)), identity)
const runCounter = compose(
increment,
increment,
increment,
decrement,
decrement,
decrement
)
console.log(runCounter(10))
Изоморфизм
Возможно, позже мы с вами обсудим, что такое изоморфизм, и поймем, как заменить ваши редьюсеры на крутящихся свинок без потери качества :)
Высокие абстракции
Все начинается с категорий и композиции. Объектами в категории может быть что угодно. Я не перестаю удивляться, как одни и те же законы могут работать для столь разных сущностей. Обычно мы смотрим на морфизмы как на некоторые функции, а на объекты как на аргументы этих функций. Но мы можем рассматривать морфизмы как объекты некоторой категории, более того мы можем собирать категории, в которых объектами будут другие категории. Разбираясь во всем этом, мы можем научиться смотреть на системы, которые мы разрабатываем чуть-чуть иначе. Это позволит нам находить более интересные и удобные абстракции для наших сущностей и их взаимодействия. Здесь я предлагаю остановиться и передохнуть. В следующих постах мы что-нибудь еще закомпозим ;)