Что общего у нормального распределения, конечных автоматов, хеш-таблиц, произвольных предикатов, строк, выпуклых оболочек, афинных преобразований, файлов конфигураций и стилей CSS? А что объединяет целые числа, типы в Haskell, произвольные графы, альтернативные функторы, матрицы, регулярные выражения и статистические выборки? Наконец, можно ли как-то связать между собой булеву алгебру, электрические цепи, прямоугольные таблицы, теплоизоляцию труб или зданий и изображения на плоскости? На эти вопросы есть два важных ответа: 1) со всеми этими объектами работают программисты, 2) эти объекты имеют сходную алгебраическую структуру: первые являются моноидами, вторые — полукольцами, третьи — алгебрами де Моргана.  Если мы, программисты, научимся сами и научим компьютер работать с моноидами, полукольцами и прочими структурами, то всем нам работать станет легче. Потому что на алгебраические структуры накладываются определённые ограничения, а из ограничений следуют и определённые гарантии. Все эти ограничения и гарантии справедливы для всех представителей структуры, а это значит, что можно строить универсальный инструментарий для работы с моноидами, полукольцами, алгебрами и, следовательно, со всеми перечисленными выше и многими другими объектами. Посмотрите, сколько математиками определено алгебраических структур! И ведь для них есть теоремы, какие-то для программистов полезные, какие-то пока не очень, но все они надёжны, как самые лучшие библиотеки программ! Всё становится ещё вкуснее, когда внутри структуры строятся гомоморфизмы — преобразования, сохраняющие структуру. Гомоморфизмы дают возможность "переключаться" с одного объекта структуры на другой — со списков на числа, с графов на матрицы, с регулярных выражений на графы, с электрических цепей на булевы выражения или на графическое изображения этих цепей! Ну а уж изоморфизмы — это вообще песня! Если программист считает что математика ему не нужна и что все эти морфизмы-шморфизмы это просто абстрактная чушь, то он лишает себя не только прекрасного, надёжного инструмента, но, самое главное, существенной части удовольствия от своей работы. В этой статье мы покажем на примере алгебр де Моргана, как строить алгебраические абстракции, как определять для них гомоморфизмы и свободные алгебры, и как это можно использовать, конечно. Статья рассчитана на тех, кто уже знает что такое классы типов, функторы, моноиды и, в целом, хорошо представляет себе что такое программирование на Haskell. С другой стороны, она может быть интересна всем, кто интересуется как в программировании строятся абстракции, и как их можно применить к реальным задачам.  Моноиды — это абстракция композиции. В свою очередь, композиция — ключевое понятие в функциональном программировании. Именно поэтому моноиды встречаются здесь так часто и играют настолько важные роли. Про ценность и богатство применения моноидов говорилось и писалось много. Здесь мы перечислим основные их свойства, которые нам пригодятся в дальнейшем изложении. Моноидом называется очень простая структура: это множество, на котором определена ассоциативная бинарная операция, имеющая единственный нейтральный элемент. Коммутативность операции не требуется, однако нейтральный элемент должен быть нейтральным, будучи как первым, так и вторым операндом.  В языке Haskell для моноидов определён класс Monoid: а библиотека Data.Monoid предоставляет ряд полезных экземпляров этого класса, таких как Sum, Product, First, Last, Endo и т.п. Ключевыми свойствами моноида являются ассоциативность и единственность нейтрального элемента. Эти ограничения позволяют обобщать моноидальные операции для произвольного порядка выполнения (в том числе, и для параллельного).  Для любого моноида можно определить дуальный ему моноид, в котором бинарная операция принимает аргументы в обратном порядке: Хорошо известно, насколько полезным и универсальным инструментом является свёртка: с одной стороны, через свёртку выражается дюжина полезных универсальных функций, обрабатывающих коллекции, а с другой, сворачивать можно списки, деревья и прочие индуктивные коллекции. Свёрток определено достаточно много. Сворачивать коллекцию можно как справа, так и слева, это может повлиять и на результат (в зависимости от ассоциативности сворачивающей функции), и на эффективность вычислений. Сворачивать можно вводя начальный результат свёртки, на случай пустой коллекции, или обходясь без него, если есть гарантии, что коллекция не пуста. Поэтому в стандартной библиотеке Haskell и библиотеке Data.Foldable так много различных свёрток: Если же речь идёт о свёртке в моноид, достаточно одной функции, в силу ассоциативности моноидальной операции и гарантии наличия нейтрального элемента. Чаще используется вариант, позволяющий явно указывать какой именно моноид следует использовать для свёртки: Если коллекция является функтором, то определение функции foldMap можно дать такое: Иногда существует возможность "переходить" между моноидами, напомним, такие преобразования называются гомоморфизмами. При этом существует такой моноид, из которого можно построить гомоморфизм в любой другой. Такой моноид называется свободным, его роль в Haskell играют списки. Свободные структуры отражают свойства алгебраической системы, но ничего "не вычисляют", не изменяют данные и не теряют информации. В известном смысле, они представляют собой формальный язык для описания элемента алгебраической системы или категории, при этом гомоморфизмы можно рассматривать в качестве интерпретаторов этого языка. Такая аналогия нам пригодится в дальнейшем. Подробнее о моноидах и их применении в Haskell можно почитать в статьях: Для многих объектов существует несколько способов композиции и одним моноидом тут не обойтись. Если таких способов два, то говорят о кольцах, полукольцах, решётках и различных алгебрах. Рассмотрим несколько примеров, наводящих на мысль об общности алгебраической структуры, лежащей в их основе. Что мы знаем о булевой алгебре:  Давайте рассмотрим задачу расчёта произвольной двухполюсной электрической цепи, которая может состоять из сопротивлений и допускает последовательное или параллельное соединение элементов цепи. 
 Эти выражения можно переписать симметричным способом:
 
 в котором узнаются законы де Моргана! Прямоугольные таблицы также можно комбинировать двумя основными способами: объединяя строки () или столбцы (): Кроме того, для таблиц можно определить инволюцию — транспонирование, и нетрудно проверить, что оба способа комбинирования таблиц образуют моноиды с одинаковым нейтральным элементом — пустой таблицей. И, наконец, один способ комбинирования можно получить из другого: например, объединить две таблицы по вертикали можно сначала транспонировав обе таблицы, затем объединив их по горизонтали и транспонировав результат.   Используя свойства инволюции, опять получаем законы де Моргана:   Пора дать формальное определение для структуры, с которой мы работаем: Алгеброй де Моргана называется структура, состоящая из множества, на котором определены две бинарные операции  и , условно, называющиеся сложением и умножением, а также операция инволюции , для которых выполняются следующие условия:       Строго говоря, умножение ещё должно быть дистрибутивно по отношению к сложению, но в некоторых случаях (например для электрических цепей) это может не выполняться. Зато если выполняется, то можно сложное выражение, состоящее из комбинации сложений и умножений привести к форме суммы одночленов, раскрыв все скобки, или, наоборот, упростить выражение, вынося за скобки общие множители. Иногда инволюция выполняет роль инверсии, то есть, порождает обратный элемент для операции умножения. Тогда выполняется следующее тождество:   Перед началом работы подключим несколько расширений: Определим для алгебр де Моргана класс типов: В этом определении мы уже используем законы де Моргана для того, чтобы упростить работу с классом. В описании экземпляра достаточно определить только одну из двух моноидальных операций, её нейтральный элемент и инволюцию. Создадим первый экземпляр класса DeMorgan для логических данных: Из соображений эффективности, конечно, стоит явно определить и оператор <|>, но мы оставим это лаконичное определение, чтобы убедиться в том, что оно в полной мере задаёт алгебру для типа Bool: Булева алгебра, это хорошо, но уж больно просто. Давайте её обобщим, построив алгебру для нечёткой логики в духе Лотфи Заде. Для этого определим тип-обёртку Fuzzy и потребуем у компилятора вывести для него числовые свойства: Определим простейшую алгебру де Моргана для таблиц, представленных вложенными списками: здесь мы использовали функцию transpose из библиотеки Data.List. Вот как работает эта алгебра: Давайте применим её для написания простенькой табулирующей функции table Наконец, вспомним о сопротивлениях и построим алгебру де Моргана для чисел, точнее, для числовых типов, для которых определена операция деления. Для этого определим тип-обёртку Frac: В качестве примера, выразим эффективное сопротивление цепи, показанной на рисунке и вычислим силу тока при напряжении 12 V:
  Для расчёта эффективной жёсткости сложной системы пружин, или ёмкости батареи конденсаторов, тоже годится тип Frac, но для этих расчётов нужно поменять местами операторы <|> и <->, а также элементы zero и one. Математики с радостью узнают в этом преобразовании переход к дуальной алгебре. Звучит красиво, но не хотелось бы писать 2 <-> 3 имея в виду параллельное соединение пружин. Было бы хорошо сохранить семантику операторов, но изменить способ вычислений. Тут математика снова даёт нам возможность воспользоваться её плодами. Как и для любого моноида, для любой алгебры де Моргана определена дуальная ей алгебра, в которой "всё наоборот". Опишем это обстоятельство на языке Haskell. Создадим новый тип-обёртку Dual для дуальных алгебр и сообщим, что если алгебра a является алгеброй де Моргана, то и дуальная ей тоже будет алгеброй де Моргана.  Теперь можно рассчитать эффективную жёсткость системы из трёх пружин, одна из которых последовательно соединяется с парой других, соединёных параллельно: Вывод типов позволяет указывать какую именно алгебру мы используем только "на выходе" из вычислений. Так мы можем явно и, в то же время, просто, изменять способ вычисления выражения. А что если мы захотим представлять и рассчитывать цепи, содержащие не только сопротивления, но и ключи, конденсаторы, катушки и т.д.? И не только рассчитывать, но и изображать их или сохранять в файл? Для этого нам понадобится свободная алгебра. Цепь будем представлять некоторой структурой данных, ничего не вычисляющей, но отражающей алгебраическую структуру цепи и образующей алгебру де Моргана: Инволюция или инверсия для элемента цепи не имеет определённого смысла, но для корректности и общности мы включили её в описание типа. Самый первый и естественный гомоморфизм — это преобразование типа Circuit в произвольный тип, для которого определена алгебра де Моргана: Функция reduce подобна функции fold, которая сворачивает список моноидов в произвольный моноид. Таким же образом, как от функции fold для функторов можно произвести функцию foldMap, можно образовать функцию reduceMap: Эта функция позволяет явно указать какую именно алгебру де Моргана следует использовать при интерпретации цепи. Согласитесь, обнаружение такой красивой симметрии в определениях функций свёртки для моноидов и нашей алгебры доставляет эстетическое удовольствие! Для построения цепей организуем предметно-ориентированный тип Lumped, который позволит нам кроме значений параметров элементов цепи Value a, явно представить короткое замыкание Short и разрыв цепи Break: Введём тип для элементов: сопротивлений, ёмкостей и индуктивностей, а также конструкторы элементов цепи: Наконец, напишем простенькую цепь для опытов. Теперь мы готовы строить различные интерпретации нашей свободной алгебры.  Функция connect определяет является ли цепь замкнутой, используя булеву алгебру: Функция resistance определяет эффективное сопротивление цепи для постоянного тока, используя алгебру для дробных чисел: Здесь вычислительная работа производится внутри типа Lumped, Который мы объявили функтором. Для расчёта импеданса цепи потребуетcя подключить модуль Data.Complex: Если речь идёт о расчёте батареи конденсаторов, то нужно поменять алгебру на дуальную: Обратите внимание на то, как во всех этих интерпретациях мы указывали способ расчёта характеристик цепи, определяя лишь способ расчёта для отдельных элементов и выбирая подходящую алгебру для свёртки.  А если нам нужно будет изобразить цепь, то можно воспользоваться великолепной библиотекой Diagrams для построения векторной графики. Она предоставляет два комбинатора для относительного расположения изображений: (|||) — рядом по горизонтали и (===) — рядом по вертикали. Это значит, что для изображений можно построить алгебру, подобную алгебре таблиц, а потом, описав как изображаются отдельные элементы цепи, построить гомоморфизм из свободной алгебры в алгебру изображений и нарисовать произвольную схему. Кстати, если вы хотите увидеть филигранное и нетривиальное использование моноидов, обратите внимание на эту библиотеку и на статьи, с ней связанные. Мы не рассмотрели всех возможностей, которые даёт использование абстракции для алгебры де Моргана. Можно было привести пример расчёта теплоизоляции здания с окнами и сложным покрытием стен; или моделирование неньютоновских жидкостей цепями из элементов жёсткости и вязкости.  Для моноидов и алгебры де Моргана выполняется ряд простых теорем, например, известно, что произведение типов-моноидов тоже образует моноид. Эвивалентное утверждение верно для алгебр де Моргана, оно даёт возможность за один проход по структуре вычислять сразу несколько характеристик. Более того, легко можно построить эпиморфизм для алгебры в моноид и вычислять не только алгебраические характеристики, но и любые моноидальные. Как самый простой пример: одновременно с расчётом сопротивлений можно вычислить суммарную мощность выделяемую цепью, которая будет описываться простым суммирующим моноидом.  Свободная алгебра даёт ещё одну возможность, существенную, например, для задач нечёткой логики. Ещё не вычисленное выражение можно упростить, если воспользоваться дистрибутивностью алгебры. Эту процедуру можно выполнить во время выполнения программы, то есть с произвольными данными, но перед интенсивными вычислениями. На что способны свободные структуры показывает Wolfram Mathematica, в которой все выражения, по существу, представляют собой свободные алгебры. По сравнению с лошадью или с автомобилем, поезд имеет существенное ограничение — он может двигаться только по железной дороге. И у железной дороги есть существенное ограничение — по ней могут двигаться только поезда. Но эти ограничения дают новые возможности — можно существенно снизить трение, увеличить скорость и построить сложную и эффективную автоматическую систему управления движением сотен составов. На автомагистрали нельзя ездить на мопеде или на тракторе. Это ограничение. Но если мы гарантируем его, то можем рассчитывать на высокие скорости и высокую пропускную способность трассы.  Функциональное программирование добавляет в повседневную практику программиста различные ограничения, и законы. Изменять состояние нельзя, композиция должна быть ассоциативной, аппликативные функторы и монады должны удовлетворять законам аппликативных функторов и монад и т.д. Это может раздражать, но вместе с этими ограничениями и законами приходят гарантии, что код будет работать именно так, а не иначе. А самое главное, именно неукоснительное следование этим законам, в принципе, и позволяет говорить в программе о функторах, монадах, ассоциативности и дистрибутивности, алгебрах и прочих категориях, а значит и пользоваться наработками многих поколений математиков и применять их в повседневной работе. Так что все эти сложные штуки — не костыли ФП, как может показаться, а ставшие доступными благодаря развитию аппаратной части компьютеров, новые возможности. Не стоит ими пренебрегать. Руководитель, научный сотрудник, преподаватель