Что общего у нормального распределения, конечных автоматов, хеш-таблиц, произвольных предикатов, строк, выпуклых оболочек, афинных преобразований, файлов конфигураций и стилей CSS? А что объединяет целые числа, типы в Haskell, произвольные графы, альтернативные функторы, матрицы, регулярные выражения и статистические выборки? Наконец, можно ли как-то связать между собой булеву алгебру, электрические цепи, прямоугольные таблицы, теплоизоляцию труб или зданий и изображения на плоскости? На эти вопросы есть два важных ответа: 1) со всеми этими объектами работают программисты, 2) эти объекты имеют сходную алгебраическую структуру: первые являются моноидами, вторые — полукольцами, третьи — алгебрами де Моргана.


Если мы, программисты, научимся сами и научим компьютер работать с моноидами, полукольцами и прочими структурами, то всем нам работать станет легче. Потому что на алгебраические структуры накладываются определённые ограничения, а из ограничений следуют и определённые гарантии. Все эти ограничения и гарантии справедливы для всех представителей структуры, а это значит, что можно строить универсальный инструментарий для работы с моноидами, полукольцами, алгебрами и, следовательно, со всеми перечисленными выше и многими другими объектами. Посмотрите, сколько математиками определено алгебраических структур! И ведь для них есть теоремы, какие-то для программистов полезные, какие-то пока не очень, но все они надёжны, как самые лучшие библиотеки программ!


Всё становится ещё вкуснее, когда внутри структуры строятся гомоморфизмы — преобразования, сохраняющие структуру. Гомоморфизмы дают возможность "переключаться" с одного объекта структуры на другой — со списков на числа, с графов на матрицы, с регулярных выражений на графы, с электрических цепей на булевы выражения или на графическое изображения этих цепей! Ну а уж изоморфизмы — это вообще песня! Если программист считает что математика ему не нужна и что все эти морфизмы-шморфизмы это просто абстрактная чушь, то он лишает себя не только прекрасного, надёжного инструмента, но, самое главное, существенной части удовольствия от своей работы.


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


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


Немного про моноиды


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


Определение


Моноидом называется очень простая структура: это множество, на котором определена ассоциативная бинарная операция, имеющая единственный нейтральный элемент. Коммутативность операции не требуется, однако нейтральный элемент должен быть нейтральным, будучи как первым, так и вторым операндом.


В языке Haskell для моноидов определён класс Monoid:


class Monoid a where
  mempty :: a
  mappend :: a -> a -> a

а библиотека Data.Monoid предоставляет ряд полезных экземпляров этого класса, таких как Sum, Product, First, Last, Endo и т.п.


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


Дуальный моноид


Для любого моноида можно определить дуальный ему моноид, в котором бинарная операция принимает аргументы в обратном порядке:


> Dual [1,2,3] <> Dual [5,6]
Dual { getDual = [5,6,1,2,3] }

Связь со свёрткой


Хорошо известно, насколько полезным и универсальным инструментом является свёртка: с одной стороны, через свёртку выражается дюжина полезных универсальных функций, обрабатывающих коллекции, а с другой, сворачивать можно списки, деревья и прочие индуктивные коллекции. Свёрток определено достаточно много. Сворачивать коллекцию можно как справа, так и слева, это может повлиять и на результат (в зависимости от ассоциативности сворачивающей функции), и на эффективность вычислений. Сворачивать можно вводя начальный результат свёртки, на случай пустой коллекции, или обходясь без него, если есть гарантии, что коллекция не пуста. Поэтому в стандартной библиотеке Haskell и библиотеке Data.Foldable так много различных свёрток:


 foldr, foldl, foldr1, foldl1, foldl', foldl1'

Если же речь идёт о свёртке в моноид, достаточно одной функции, в силу ассоциативности моноидальной операции и гарантии наличия нейтрального элемента.


fold :: (Monoid m, Foldable t) => t m -> m 

Чаще используется вариант, позволяющий явно указывать какой именно моноид следует использовать для свёртки:


foldMap :: (Monoid m, Foldable t) => (a -> m) -> t a -> m

Если коллекция является функтором, то определение функции foldMap можно дать такое:


foldMap f = fold . fmap f

Свободный моноид


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


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


Подробнее о моноидах и их применении в Haskell можно почитать в статьях:



Алгебры де Моргана


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


Булева алгебра


Что мы знаем о булевой алгебре:


  • Определена на множестве значений {True, False} и описывается с помощью трёх операций: конъюнкции, дизъюнкции и отрицания.
  • Конъюнкция и дизъюнкция формируют моноиды.
  • Элемент False является нейтральным для дизъюнкции и поглощающим для конъюнкции.
  • Элемент True, в свою очередь, нейтральный элемент для конъюнкции и поглощающий для дизъюнкции.
  • Отрицание является инволюцией, то есть, эта операция обратна самой себе.
  • Выполняется закон де Моргана.

$! (A \wedge B) = ! A \vee ! B,\quad ! (A \vee B) = ! A \wedge ! B$


Алгебра электрических сопротивлений


Давайте рассмотрим задачу расчёта произвольной двухполюсной электрической цепи, которая может состоять из сопротивлений и допускает последовательное или параллельное соединение элементов цепи.


  • Для них определены две операции последовательного ($\leftrightarrow$) и параллельного ($\updownarrow$) соединения.
  • Соединения формируют моноиды (они ассоциативны и имеют нейтральные элементы).
  • Разрыв цепи является нейтральным для параллельного и поглощающим для последовательного соединений.
  • Короткое замыкание является нейтральным для последовательного и поглощающим для параллельного соединений.
  • Сопротивлениям соответствует обратная величина — проводимость и переход от сопротивлений к проводимости является инволюцией.
  • Эффективное сопротивление для последовательного и параллельного соединений вычисляется по формулам:

    $R_1 \leftrightarrow R_2 = R_1 + R_2,\quad R_1 \updownarrow R_2 = \frac{1}{\frac{1}{R_1} + \frac{1}{R_2}}.$

    Эти выражения можно переписать симметричным способом:

    $\frac{1}{R_1 \updownarrow R_2} = \frac{1}{R_1} \leftrightarrow \frac{1}{R_2},\quad\frac{1}{R_1 \leftrightarrow R_2} = \frac{1}{R_1} \updownarrow \frac{1}{R_2},$

    в котором узнаются законы де Моргана!



    Алгебра таблиц


    Прямоугольные таблицы также можно комбинировать двумя основными способами: объединяя строки ($\leftrightarrow$) или столбцы ($\updownarrow$):


    1 2 3       a b     1 2 3 a b
    4 5 6  <->  c d  =  4 5 6 c d
    7 8 9       e f     7 8 9 e f
    
    1 2 3       a b c     1 2 3
    4 5 6  <|>  d e f  =  4 5 6
    7 8 9                 7 8 9
                          a b c
                          d e f

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


    $ A \leftrightarrow B = (A^\top \updownarrow B^\top)^\top,\quad A \updownarrow B = (A^\top \leftrightarrow B^\top)^\top. $


    Используя свойства инволюции, опять получаем законы де Моргана:


    $ (A \leftrightarrow B)^\top = A^\top \updownarrow B^\top,\quad (A \updownarrow B)^\top = A^\top \leftrightarrow B^\top. $


    Определение алгебры де Моргана


    Пора дать формальное определение для структуры, с которой мы работаем:


    Алгеброй де Моргана называется структура, состоящая из множества, на котором определены две бинарные операции $+$ и $\times$, условно, называющиеся сложением и умножением, а также операция инволюции $'$, для которых выполняются следующие условия:


    • операция $+$ образует моноид с нейтральным элементом $inline$0$inline$:

      $a + 0 = 0 + a = a,\quad a + (b + c) = (a + b) + c;$


      операция $\times$ образует моноид с нейтральным элементом $1$:

      $a \times 1 = 1 \times a = a,\\ a \times (b \times c) = (a \times b) \times c;$


      выполняется мультипликативное свойство нуля:

      $0\times x = x \times 0 = 0;$


      свойство инволюции:

      $(x')' = x;$


      дуальность нуля и единицы относительно инволюции:

      $0' = 1,\\ 1' = 0;$


      законы де Моргана:

      $(A + B)' = A' \times B',\\ (A \times B)' = A' + B'.$



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


      Иногда инволюция выполняет роль инверсии, то есть, порождает обратный элемент для операции умножения. Тогда выполняется следующее тождество:

      $x \times x' = x' \times x = 1.$


      Это выполняется для булевой алгебры и алгебры сопротивлений, но неверно для таблиц.


      Абстракция алгебры де Моргана на Haskell


      Перед началом работы подключим несколько расширений:


      {-# LANGUAGE DeriveFunctor, FlexibleInstances, GeneralizedNewtypeDeriving #-}

      Определим для алгебр де Моргана класс типов:


      class DeMorgan a where
        {-# MINIMAL inv,((<->)|(<|>)),(zero|one) #-}
        inv :: a -> a
      
        zero :: a
        zero = inv one
      
        one :: a
        one = inv zero
      
        (<->) :: a -> a -> a
        a <-> b = inv (inv a <|> inv b)
      
        (<|>) :: a -> a -> a
        a <|> b = inv (inv a <-> inv b)

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


      Создадим первый экземпляр класса DeMorgan для логических данных:


      instance DeMorgan Bool where
        zero = False
        inv = not
        (<->) = (&&)

      Из соображений эффективности, конечно, стоит явно определить и оператор <|>, но мы оставим это лаконичное определение, чтобы убедиться в том, что оно в полной мере задаёт алгебру для типа Bool:


      > True <|> False
      True
      > one :: Bool
      True

      Булева алгебра, это хорошо, но уж больно просто. Давайте её обобщим, построив алгебру для нечёткой логики в духе Лотфи Заде. Для этого определим тип-обёртку Fuzzy и потребуем у компилятора вывести для него числовые свойства:


      newtype Fuzzy = Fuzzy { deFuzzify :: Double }
        deriving (Show, Num, Fractional, Ord, Eq)
      
      instance DeMorgan Fuzzy where
        zero = 0
        inv x = fuzzify $ 1 - x
        a <|> b = fuzzify $ max a b
      
      fuzzify x = 0 `max` x `min` 1

      > deFuzzify $ one
      1
      > deFuzzify $ 0.2 <-> 0.4
      0.2 
      > deFuzzify $ 0.8 <|> 0.4 <-> 0.5
      0.5
      > deFuzzify $ 0.3 <|> 0.4 <-> 0.5
      0.4

      Определим простейшую алгебру де Моргана для таблиц, представленных вложенными списками:


      instance DeMorgan [[a]] where
        zero = []
        (<|>) = (++)
        inv = transpose

      здесь мы использовали функцию transpose из библиотеки Data.List. Вот как работает эта алгебра:


      > inv [[1,2],[3,4]]
      [[1,3],[2,4]]
      > [[1,2],[3,4]] <-> [[5],[6]]
      [[1,2,5],[3,4,6]]
      > [[1,2],[3,4]] <|> [[5,6]]
      [[1,2],[3,4],[5,6]]

      Давайте применим её для написания простенькой табулирующей функции table


      table :: (Show a, Show b) => (a -> a -> b) -> [a] -> [[String]]
      table f vals = ([[" "]] <-> h) <|> (inv h <-> c)
        where
          h = [show <$> vals]
          c = [show <$> [ f x y | x <- vals] | y <- vals]
      
      showTable tbl = putStrLn $ unlines $ unwords <$> tbl

      > showTable $ table mod [1..6]
        1 2 3 4 5 6
      1 0 0 0 0 0 0
      2 1 0 1 0 1 0
      3 1 2 0 1 2 0
      4 1 2 3 0 1 2
      5 1 2 3 4 0 1
      6 1 2 3 4 5 0

      Наконец, вспомним о сопротивлениях и построим алгебру де Моргана для чисел, точнее, для числовых типов, для которых определена операция деления. Для этого определим тип-обёртку Frac:


      newtype Frac a = Frac {getFrac :: a}
        deriving (Show, Num, Fractional, Functor)
      
      instance Fractional a => DeMorgan (Frac a) where
        zero = 0
        inv = (1/)
        (<->) = (+)

      > getFrac $ 2 <-> 6
      8.0
      > getFrac $ 2 <|> 6
      1.5
      > getFrac $ 1 <-> (2  <|> (1 <-> 1))
      2.0

      В качестве примера, выразим эффективное сопротивление цепи, показанной на рисунке и вычислим силу тока при напряжении 12 V:
      image


      > getFrac $ ((6 <|> 12) <-> 4) <|> (3 <-> 5)
      4.0
      > getFrac $ 12 / ((6 <|> 12) <-> 4) <|> (3 <-> 5)
      3.0

      Дуальная алгебра


      Для расчёта эффективной жёсткости сложной системы пружин, или ёмкости батареи конденсаторов, тоже годится тип Frac, но для этих расчётов нужно поменять местами операторы <|> и <->, а также элементы zero и one. Математики с радостью узнают в этом преобразовании переход к дуальной алгебре. Звучит красиво, но не хотелось бы писать 2 <-> 3 имея в виду параллельное соединение пружин. Было бы хорошо сохранить семантику операторов, но изменить способ вычислений.


      Тут математика снова даёт нам возможность воспользоваться её плодами. Как и для любого моноида, для любой алгебры де Моргана определена дуальная ей алгебра, в которой "всё наоборот". Опишем это обстоятельство на языке Haskell. Создадим новый тип-обёртку Dual для дуальных алгебр и сообщим, что если алгебра a является алгеброй де Моргана, то и дуальная ей тоже будет алгеброй де Моргана.


      newtype Dual a = Dual {getDual :: a}
        deriving (Show, Num, Fractional, Eq, Ord, Functor)
      
      instance DeMorgan a => DeMorgan (Dual a) where
        zero = Dual one
        one = Dual zero
        inv x = inv <$> x
        Dual a <|> Dual b = Dual $ a <-> b
        Dual a <-> Dual b = Dual $ a <|> b

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


      > getFrac . getDual $ 600 <-> (200 <|> 300)
      450.0

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


      Свободная алгебра


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


      Цепь будем представлять некоторой структурой данных, ничего не вычисляющей, но отражающей алгебраическую структуру цепи и образующей алгебру де Моргана:


      data Circuit a = Zero
                     | One
                     | Elem a
                     | Inv (Circuit a)
                     | Par (Circuit a) (Circuit a)
                     | Seq (Circuit a) (Circuit a)
        deriving (Show, Functor)</hs></pre>
      
      instance DeMorgan (Circuit a) where
        zero = Zero
        one = One
        (<->) = Seq
        (<|>) = Par
        inv = Inv

      Инволюция или инверсия для элемента цепи не имеет определённого смысла, но для корректности и общности мы включили её в описание типа.


      Самый первый и естественный гомоморфизм — это преобразование типа Circuit в произвольный тип, для которого определена алгебра де Моргана:


      reduce :: DeMorgan a => Circuit a -> a
      reduce circ = case circ of
        Zero -> zero
        One -> one
        Elem b -> b
        Inv a -> inv (reduce a)
        Par a b -> reduce a <|> reduce b
        Seq a b -> reduce a <-> reduce b

      Функция reduce подобна функции fold, которая сворачивает список моноидов в произвольный моноид. Таким же образом, как от функции fold для функторов можно произвести функцию foldMap, можно образовать функцию reduceMap:


      reduceMap :: DeMorgan b => (a -> b) -> Circuit a -> b
      reduceMap = reduce . fmap f

      Эта функция позволяет явно указать какую именно алгебру де Моргана следует использовать при интерпретации цепи.


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


      Различные задачи расчёта цепей


      Для построения цепей организуем предметно-ориентированный тип Lumped, который позволит нам кроме значений параметров элементов цепи Value a, явно представить короткое замыкание Short и разрыв цепи Break:


      data Lumped a = Short
                    | Value a
                    | Break
        deriving (Show, Functor)
      
      instance DeMorgan s => DeMorgan (Lumped s) where
        zero = Break
      
        Value r1 <-> Value r2 = Value $ r1 <-> r2
        Short <-> r = r
        r <-> Short = r
        _ <-> Break = Break
        Break <-> _ = Break
      
        inv Short = Break
        inv Break = Short
        inv (Value r) = Value (inv r)

      Введём тип для элементов: сопротивлений, ёмкостей и индуктивностей, а также конструкторы элементов цепи:


      data Element = R Double
                   | C Double
                   | L Double
                   deriving Show
      
      res = Elem . R
      cap = Elem . C
      coil = Elem . L
      key True = Zero
      key False = One

      Наконец, напишем простенькую цепь для опытов.


      s :: Bool -> Circuit Element
      s k = res 10 <-> ((res 2 <-> coil 5e-3 <-> key k) <|> cap 10e-9)

      Теперь мы готовы строить различные интерпретации нашей свободной алгебры.


      Функция connect определяет является ли цепь замкнутой, используя булеву алгебру:


      connected :: Circuit Element -> Bool
      connected = reduceMap f
        where
          f el = case el of
            R _ -> True
            C _ -> False
            L _ -> True

      > connected (s True)
      True
      > connected (s False)
      False

      Функция resistance определяет эффективное сопротивление цепи для постоянного тока, используя алгебру для дробных чисел:


      resistance :: Circuit Element -> Lumped Double
      resistance = fmap getFrac . reduceMap (fmap Frac . f)
        where
          f el = case el of
            R r -> Value r
            C _ -> Break
            L _ -> Short

      Здесь вычислительная работа производится внутри типа Lumped, Который мы объявили функтором.


      > resistance (s True)
      Value 12
      > resistance (s False)
      Break

      Для расчёта импеданса цепи потребуетcя подключить модуль Data.Complex:


      impedance :: Double -> Circuit Element -> Lumped (Complex Double)
      impedance w = fmap getFrac . reduceMap (fmap Frac . f)
        where
          f el = Value $ case el of
            R r -> r :+ 0
            C c -> 1 / (0 :+ w*c)
            L l -> 0 :+ w*l

      > impedance 1e3 (s False)
      Value (10.0 :+ (-99999.99999999997))
      > impedance 1e3 (s True)
      Value (12.00020001420084 :+ 5.0002100065000405)
      > impedance 1e6 (s False)
      Value (10.0 :+ (-100.0))
      > impedance 1e6 (s True)
      Value (10.000832986116954 :+ (-102.04081598653629))

      Если речь идёт о расчёте батареи конденсаторов, то нужно поменять алгебру на дуальную:


      capacity :: Circuit Element -> Lumped Double
      capacity = fmap (getFrac . getDual) . reduceMap (fmap (Dual . Frac) . f)
        where
          f el = case el of
            R _ -> Short
            C c -> Value c
            L _ -> Short

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


      А если нам нужно будет изобразить цепь, то можно воспользоваться великолепной библиотекой Diagrams для построения векторной графики. Она предоставляет два комбинатора для относительного расположения изображений: (|||) — рядом по горизонтали и (===) — рядом по вертикали. Это значит, что для изображений можно построить алгебру, подобную алгебре таблиц, а потом, описав как изображаются отдельные элементы цепи, построить гомоморфизм из свободной алгебры в алгебру изображений и нарисовать произвольную схему. Кстати, если вы хотите увидеть филигранное и нетривиальное использование моноидов, обратите внимание на эту библиотеку и на статьи, с ней связанные.


      Заключение


      Мы не рассмотрели всех возможностей, которые даёт использование абстракции для алгебры де Моргана. Можно было привести пример расчёта теплоизоляции здания с окнами и сложным покрытием стен; или моделирование неньютоновских жидкостей цепями из элементов жёсткости и вязкости.


      Для моноидов и алгебры де Моргана выполняется ряд простых теорем, например, известно, что произведение типов-моноидов тоже образует моноид. Эвивалентное утверждение верно для алгебр де Моргана, оно даёт возможность за один проход по структуре вычислять сразу несколько характеристик. Более того, легко можно построить эпиморфизм для алгебры в моноид и вычислять не только алгебраические характеристики, но и любые моноидальные. Как самый простой пример: одновременно с расчётом сопротивлений можно вычислить суммарную мощность выделяемую цепью, которая будет описываться простым суммирующим моноидом.


      Свободная алгебра даёт ещё одну возможность, существенную, например, для задач нечёткой логики. Ещё не вычисленное выражение можно упростить, если воспользоваться дистрибутивностью алгебры. Эту процедуру можно выполнить во время выполнения программы, то есть с произвольными данными, но перед интенсивными вычислениями. На что способны свободные структуры показывает Wolfram Mathematica, в которой все выражения, по существу, представляют собой свободные алгебры.




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


      Функциональное программирование добавляет в повседневную практику программиста различные ограничения, и законы. Изменять состояние нельзя, композиция должна быть ассоциативной, аппликативные функторы и монады должны удовлетворять законам аппликативных функторов и монад и т.д. Это может раздражать, но вместе с этими ограничениями и законами приходят гарантии, что код будет работать именно так, а не иначе. А самое главное, именно неукоснительное следование этим законам, в принципе, и позволяет говорить в программе о функторах, монадах, ассоциативности и дистрибутивности, алгебрах и прочих категориях, а значит и пользоваться наработками многих поколений математиков и применять их в повседневной работе. Так что все эти сложные штуки — не костыли ФП, как может показаться, а ставшие доступными благодаря развитию аппаратной части компьютеров, новые возможности. Не стоит ими пренебрегать.

Поделиться с друзьями
-->

Комментарии (12)


  1. shuhray
    14.03.2017 01:56
    +4

    Если назвать моноид «полугруппой с единицей», становится не так страшно. Дописал учебник теории категорий
    https://github.com/George66/Textbook


    1. samsergey
      14.03.2017 02:17

      А разве так страшно? :) Термин моноид мне нравится больше чем полугруппа с единицей. Второй термин сложный (в лингвистическом смысле) — он состоит из двух понятий, каждое из которых требует определения. Более того, полугруппа — тоже сложный термин, наводящий на мысль о том, что же такое группа, а она, увы, определяется вовсе не так просто и несколько уводит в сторону от понятия моноид.

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

      Хорошее слово, ёмкое. И означающее что-то одно и фундаментальное. В «полугруппе с единицей» есть что-то неполноценное, и группа-то наполовинку, и единицы могло бы не быть… :)

      Поздравляю с окончанием работы над учебником!


      1. shuhray
        14.03.2017 03:20

        Вот именно потому, что похоже на слово «монада». Полугруппа с единицей — вещь достаточно простая, к монаде сама по себе имеет мало отношения (а к группе много). Группы надо бы знать в любом случае (на популярном уровне книжки с картинками Александрова). Слово «моноид», мне кажется, лучше использовать для некоторого более сложного обобщения (как в книжке Маклейна).


        1. samsergey
          14.03.2017 03:41

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


    1. Ares_ekb
      14.03.2017 06:07

      Очень клевый у вас учебник, буду везде рекламировать :) Хотя, чувствую, что я так и не пойму адъюнкции и лемму Йонеды. Все эти формулы, диаграммы я конечно понимаю. Но мне не хватает каких-то аналогий из программирования, более конкретных примеров. Например, в своей статье я описывал универсальное свойство, (ко)пределы через поиск минимального или максимального множества в категории множеств. Это очень частный пример, но зато он понятен и от него можно переходить к чему-то более сложному. На мой взгляд, для программистов такой вычислительный взгляд на теорию категорий единственный возможный. Программисты и математики мыслят совершенно по-разному. Я смутно понимаю, что адъюнкции — это частный случай универсального свойства. Универсальное свойство, которые выполняется для всех объектов в категории. Но пока на конкретных множествах в этом не убедишься, всё это пустой звук.

      В вашей книге есть глава про исчисление высказываний. С точки зрения программиста это можно использовать для описания семантики языков программирования. Интересно, вам попадались такие примеры? Чтобы для какого-то простенького языка программирования семантика была описана полностью с помощью теории категорий? По денотационной семантике таких учебников полно, а по категориальной ещё ни разу не встречал.


      1. shuhray
        14.03.2017 07:16

        Я сам научился теории категорий, читая несколько лет бестолковую книжку Голдблатта и научные статьи в ГПНТБ. По мере научения пробовал на людях (на маленьком семинаре в МГУ). Ситуация такая: теорию категорий у нас немножко знают алгебраисты, но им нужен довольно специальный кусок для их специальных целей. Что такое «топос», например, они не знали. Я решил примеры брать в основном из самой теории категорий (множества, меняющиеся со временем и т.п.), поскольку красивые картинки, все конструкции наглядны, а специальных программистских примеров я всё равно много не подберу, поскольку не программист. Есть много англоязычных учебников теории категорий специально для программистов — надеюсь, теперь их легче будет переводить.


        1. beavis88
          14.03.2017 10:22

          Бестолковая??? Да это лучшая книга по теоркату которые мне приходилось видеть.

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


          1. shuhray
            14.03.2017 10:58

            Однако, встречал только одного человека (себя), который сумел научиться по книжке Голдблатта. Вы второй! На русском языке по теоркату есть алгебраические книжки про гомотопии, есть книга Джонстона, по которой учиться нельзя (она для профессионалов) и Голдблатт, в этом смысле он был лучший.


      1. shuhray
        14.03.2017 07:29

        А по поводу программирования с помощью категорий приходит на ум «категорная абстрактная машина» (наберите на libgen фамилию Curien)


        1. Ares_ekb
          14.03.2017 08:53

          Я тоже учился сам, прочитал/пролистал много книг и статей, из которых мало чего понял. Действительно что-то соображать начал только после:
          D.E. Rydeheard, R.M. Burstall. Computational Category Theory
          H.J. Schneider. Graph Transformations. An Introduction to the Categorical Approach

          Curien не читал и про libgen не слышал, спасибо! Есть языки программирования, основанные полностью на теории категорий. Но хочется что-то типа D. Schmidt. Denotational Semantics: A Methodology for Language Development. Только чтобы вместо множеств, доменов и монотонных функций использовалась теория категорий. Чтобы описывался какой-нибудь простой язык программирования буквально с несколькими видами выражений и операторов (объявление переменных, циклы, условия, арифметика). И чтобы описывалось как с помощью теории категорий описать семантику этого языка. Все статьи, какие видел по этой теме, либо слишком сложные, либо в них описываются частности. Ну, видимо, тема пока слишком специфическая, буду пока использовать денотационную семантику.


    1. gearbox
      14.03.2017 20:54

      жОсткий вы человек ) Есть же https://www.gitbook.com/ для таких целей.


  1. samsergey
    14.03.2017 02:14

    Удалено