7/16/2022

Функциональные штуки . Композиция

Функциональное

Про функциональный подход говорят часто и много. Кто-то считает, что это бесполезная ерунда, кто-то считает, что есть особая секта поклонников функционального программирования с тайной символикой и паролями вида "монада — это моноид в категории эндофункторов". Начинающим фронтятам говорят: чтобы пройти собес, нужно знать про чистые функции ☝️. К сожалению, им не всегда объясняют, зачем они нужны. Еще очень любят рассказывать про различие в императивном и декларативном подходах. Я, кстати, тоже так делаю :)

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

Люди и код

Код пишут люди :) У людей весьма ограничена память, поэтому (большинство) писателей кода не создают огромные модули на тысячу строк. Чтобы как-то управлять сложностью, нам нужно разбивать написанное на кусочки и удерживать их в голове. Также нам нужен какой-то способ строить из кусочков конструкции посложнее. Тогда мы можем забыть о внутренней структуре кусочка и сосредоточиться на форме нашей конструкции. Если придерживаться определенных правил композиции кусочков, мы сможем получить крутые спецэффекты и интересные конструкции. Посему переходим к композиции.

Категории и Композиция

Пусть у нас есть набор объектов. Пусть эти объекты каким-то образом связаны друг с другом. В теории категорий эти связи называют морфизмами (или стрелками).

Давайте на примере. Пусть у нас есть свинка.

Свинка? Идея утащена из книжки «Category Theory For Programmers»

Есть замечательная книжка где автор (Bartosz Milewski) объясняет теорию категорий на свинках и стрелках. Я насколько вдохновилась этой книжкой, что решила для иллюстраций тоже использовать хрюшечек :)

Повернем свинку.

А теперь проделаем это несколько раз.

У нас получилась категория со свинками, морфизмами в которой являются углы поворота хрюш.

Эти стрелочки мне что-то напоминают 🤔

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

Чтобы категория действительно была категорией, нам нужно, чтобы выполнялось 4 условия.

1. Морфизмы можно композить

Более строго: Для любых двух морфизмов

f=pig1pig2f = pig1 \to pig2
g=pig2pig3g = pig2 \to pig3

должен существовать третий – их композиция

h=pig1pig3h = pig1 \to pig3

Мы обозначаем композицию вот так:

h=gfh = g\circ f
Какой-то порядок странный у этой вашей композиции

Сначала мне казалось зашкварным, что мы сначала пишем g, а потом f. Но давайте добавим аргумент к каждой функции. Кажется, теперь все встало на свои места :)

gfg(f(x))g\circ f \to g(f(x))

2. Композиция морфизмов ассоциативна

h(gf)=(hg)fh\circ (g\circ f)=(h\circ g)\circ f
Демонстрирую на свинках

Давайте проверим, что для свинок это выполняется. Если мы повернем свинку на 120 = (30 + 90) градусов, потом на 60, то получится свинка вверх тормашками.

60(3090)=6012060\circ (30\circ 90) = 60\circ 120

То же самое получится, если мы сначала повернем свинку на 90, потом еще на 90.

(6030)90=9090(60\circ 30)\circ 90 = 90\circ 90

Мы предполагаем что все изображения свинок идентичны и каждый поворот происходит вокруг центра картинки (свинка крутится вокруг своего 🐽).

В нашей категории композиция — это просто сложение углов поворота.

3. В нашей категории есть тождественные морфизмы (identity), которые отображают объекты сами в себя

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

idpig:pigpig\mathrm {id} _{pig}: pig \to pig
Угадайте, что является тождественным морфизмом в категории со свинками

Поворот на 360°, 2 * 360°, 3 * 360° и т.д.

4. Тождественный морфизм не влияет на получившийся результат

fidpig1=idpig2f=ff\circ \mathrm {id} _{pig1}=\mathrm {id} _{pig2}\circ f=f

В нашем примере все четыре условия выполняются. Мы получили хрюшекатегорию :)

Еще один пример (откровенно говоря, притянутый за уши :)

Думаете, что крутящиеся поросята не очень помогут в разработке? Окей, давайте по-другому. Каждый мечтает сделать свой маленький 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))
Изоморфизм

Возможно, позже мы с вами обсудим, что такое изоморфизм, и поймем, как заменить ваши редьюсеры на крутящихся свинок без потери качества :)

Высокие абстракции

Все начинается с категорий и композиции. Объектами в категории может быть что угодно. Я не перестаю удивляться, как одни и те же законы могут работать для столь разных сущностей. Обычно мы смотрим на морфизмы как на некоторые функции, а на объекты как на аргументы этих функций. Но мы можем рассматривать морфизмы как объекты некоторой категории, более того мы можем собирать категории, в которых объектами будут другие категории. Разбираясь во всем этом, мы можем научиться смотреть на системы, которые мы разрабатываем чуть-чуть иначе. Это позволит нам находить более интересные и удобные абстракции для наших сущностей и их взаимодействия. Здесь я предлагаю остановиться и передохнуть. В следующих постах мы что-нибудь еще закомпозим ;)