Ранее, в материале "Абстрактная алгебра в действии" я привёл некоторые примеры алгебраического подхода в программировании. Публикацию восприняли относительно хорошо, поэтому в этой заметке продолжится развитие мысли о том, что некоторые задачи, хоть так и не кажется на первый взгляд, на самом деле, могут быть решены алгебраическим способом. Сегодня мы продвинемся дальше в вопросе знакомства с абстрактной алгеброй и посмотрим на новые примеры кода с её применением. В прошлый раз мы познакомились с такими понятиями как полугруппа и моноид. Как можно догадаться, на этом алгебра не закончилась. Чтобы двигаться дальше, как было понятно из предыдущей заметки, нам надо накидывать ограничения на операцию, определённой над носителем структуры. Но как понять в каком направлении двигаться? Вообще, весь интерес к этой науке возник из-за вопросов, касающихся уравнений. Какие это были вопросы, для нас сейчас не важно. Важно обратить внимание на слово "уравнения".  Начинаем входить в контекст. Здесь и далее имеем ввиду, что у нас есть структура и некая абстрактная бинарная операция. Имеем пару , которую обзовём моноидом. Значит, мы можем взять из любые и  , сделать и получить некоторое . Хорошо. А что если перед нами взяли и поставили такой вопрос? А мы не можем на него ответить. Просто потому что в моноиде нет инструментов для решения такой задачи. Ну, то есть, мы можем сказать, что , где называется обратным для элементом, а сам - обратимый. Или, по-научному, называется обратимым элементом, если: Но мы не можем гарантировать обратимость каждого элемента моноида. Хотя, есть вариант выкрутиться из этой ситуации... Очевидно, что не все элементы моноида обратимы. И также очевидно, что моноид может содержать такие элементы, а они в свою очередь образуют некое подмножество. Это подмножество называется "группа". Группа это моноид, в котором для каждого элемента найдётся обратный. Это и есть следующее ограничение, уже третья по счёту аксиома. И теперь наше уравнение , будучи помещённым в группу, всегда имеет решение. Чем интересны группы? Как вы уже могли догадаться тем, что для каждого объекта, неважно какой он природы, всегда найдётся его антипод. Его обратный объект. Его реверс. Его симметрия. Кстати, вы часто могли слышать, что теория групп - это наука о симметриях.  Кстати, забавно, но существует так называемая симметрическая группа . Это группа, которая содержит все биекции, с помощью которых можно поменять местами элементы множества размера . Самый легко читаемый способ представления элемента симметрической группы - это две строки чисел. Верхняя - числа , а снизу - на какое место они встают. Например, возьмём произвольный элемент : Это значит, что первый элемент встанет на второе место, второй на третье и третий на первое. Операция (композиция) определятся очень просто - . Обратный элемент тоже находится просто: достаточно поменять местами строки и переупорядочить -  .   Также, чтобы наше уравнение могло иметь решение , операция  должна обладать свойством коммутативности (то самое "от перемены мест слагаемых сумма не меняется"). И оказывается, что достаточно широкий класс групп можно сузить до коммутативных, так называемых абелевых групп. Абелева группа это группа, в которой: Но это информация для общего развития необходимая для того, чтобы пощупать концепцию. Теперь направим поток сознания в более прикладное русло. Допустим, мы хотим сделать некий каталог. Такую сущность, где можно было бы добавлять/удалять элементы, а хранение организовано по принципу append-only базы данных. То есть по сути мы можем только записывать информацию о неких изменениях. О событиях, произошедших в каталоге. При этом к самому каталогу есть требование, чтобы по элементам можно было пройтись и получить некий агрегат. Понятно, что при агрегации элементы каталога будут отображаться в некий элемент моноида, чтобы их можно было агрегировать. При чём здесь группы?  В поставленной задаче прослеживается некий сюжет о симметрии. То есть, мы можем добавлять, а можем и удалять. При этом эти действия противоположны друг другу. И при преобразовании события в некий агрегирующий элемент мы можем гарантировать сохранение этой концепции с помощью группы. Собственно в методе Reduce и происходит то, что было описано. Чтобы гарантировать агрегацию, мы говорим: map происходит в некий моноид, и сама агрегация проводится через расширение Sum, известное по прошлой статье. А затем сужаем моноид до группы чтобы Remove событиям сопоставлять "реверс" объекта. Пощупаем наш каталог.  В самом каталоге мы как-то манипулировали со строками. Получим например длину строки после манипуляций Согласен, пока потребность в каталоге не очень мотивирована, но зато отчётливо видно, что оно работает. Ок, а что если мы захотим например на основе этих записей собрать строку, которая получится в результате таких операций? Да, у строк есть конкатенация, но с ней получается только моноид. Трудно придумать так называемый "минус" для строк. Конечно, интуитивно мы представляем себе что-то вроде: Но вот что делать, если попался кейс "abc" - "d", или "" - "", или "" - "xyz"? Ответа на вопрос в таких условиях не существует, но, как это обычно бывает выход есть всегда. У многих к этому моменту могла возникнуть идея раскручивать историю о множествах. Мы знаем, что string это IEnumerable<char>, и получается начать отталкиваться от проведения аналогии с множествами, и раскрутить её до коллекции.  Однако, множества с операцией объединения образуют лишь коммутативный моноид (оставлю доказательство этого утверждения в качестве упражнения читателю). Мы можем получить абелеву группу, если вместо объединения возьмём симметрическую разницу, но поведение этой операции немного не то. Оказывается, что если мы будем отдельно вести учёт включений и исключений, то и вовсе добьёмся желаемого поведения. Делается это через представление множества в виде пары двух множеств: слева элементы, которые надо включить; справа, которые надо исключить.  Такая логика очень просто переносится на списки, при чём мы можем выбрать - исключать с конца списка или с его начала.   Да, классы "парных" структур почти идентичные, но особо красиво и не напишешь. И теперь, вернувшись к нашему каталогу, мы можем проверить всё описанное. Можно ещё поиграться и убедиться в магии происходящего. Также, если мы хотим агрегировать события каталога таким образом, чтобы получить итоговую коллекцию, такой финт тоже осуществим. Достаточно каждый объект перевести в одноэлементный парный список. И тогда можно написать такое расширение к каталогу. Такое сработает даже для какой-нибудь dto'шки. Сделаем для неё каталог и поиграемся с ним. Элементарно, размер коллекции (не каталога). Можно взять саму коллекцию и что-то с ней сделать. Или потрогать логи. Что-то на группах сильно задержались, пора двигаться дальше. С древних времён до наших дней дошёл очень известный и интересный факт. Если имеется прямоугольный треугольник со сторонами , то они удовлетворяют равенству . Данное утверждение - это теорема Пифагора, знакомая, наверно, почти каждому школьнику.  При этом у людей всегда был интерес к  рассмотрению в рамках этой теоремы таких прямоугольных треугольников, у которых стороны целые. Какое-то время спустя возникло описание всех таких треугольников, но прогресс на этом не останавливается.  Любого математика и программиста, на мой взгляд, объединяет желание обобщать. И прогресс пошёл в сторону обобщения по показателю степени: оказывается, что написанная выше формула - это частный случай Диофантова уравнения . И в 1637 году, математик Пьер Ферма сформулировал свою Великую Теорему, которая говорит о том, что таких целых троек не найти для натуральных . Это историческое событие, можно сказать, положило начало теории колец. Дело в том, что первые попытки доказательства осуществлялись для конкретных значений  и базировались на изучении делимости в множествах вида . В результате рассмотрения таких множеств математики пришли к выводу, что их элементы ведут себя как целые числа, и возникла необходимость в обобщении их арифметики. Так возникло понятие кольца. Кольцо (коммутативное) - это множество  с двумя бинарными операциями  и  такое, что:  - абелева группа; - (коммутативный) моноид;   дистрибутивно относительно  -  (при чём как слева, так и справа). Дальнейшие примеры будут менее приближенными к реальности, может быть даже в какой-то степени игрушечными, но не менее занимательными и интересными. Как уже было сказано, кольца обобщают арифметику целых чисел, то есть, их разумно вводить в тех случаях, когда нужно обобщить умножение. Поэтому хотелось бы иметь дело с не числовыми структурами. Начнём издалека, со знакомства с таким понятием, как гомоморфизм. Гомоморфизм -  это отображение между двумя структурами, которое сохраняет операцию. Например, есть две структуры:  и Тогда, отображение  называется гомоморфизмом, если Самое главное для понимания то, что гомоморфизм - это абстрактная штука, целый класс отображений. Потому что они бывают сюръективные, инъективные, в себя и т. д.  Самым важным гомоморфизмом является изоморфизм(биекция, сохраняющая операцию). Но почему? Потому что помимо того, что у двух структур одинаковое поведение операции, есть взаимно-однозначное соответствие между ними. А значит не важно, с элементами какой структуры работать для описания её свойств, потому что мы всегда можем прыгать туда-сюда.  Например, вещественные числа по сложению  образуют группу, и положительные вещественные по умножению тоже. Между ними существует изоморфизм  (поэлементная нотация отображения). И надо ли нам рассматривать каждую группу по отдельности, когда можно описать одну, а потом сказать, что вторая это первая с точностью до изоморфизма? Вопрос риторический. Есть также другой интересный класс гомоморфизмов, это эндоморфизмы. Эндоморфизм - это гомоморфизм в себя. Множество всех эндоморфизмов структуры  обозначается ,  а если  - абелева группа, то  - кольцо. Для этого сложение надо определить поточечно, а в качестве умножения взять композицию функций. Поиграемся с End<T, G> и создадим например функцию, генерирующую множество степеней двойки. Перед этим добавим одноэлементный конструктор в PairedHashSet<T> и напишем расширение для кольца, считающее степень.  Как и в случае с группой у кольца есть свой "полу" аналог. Всё также есть тройка , но требуют от неё других вещей:  - коммутативный моноид;  - моноид; Умножение дистрибутивно относительно сложения; Для всех элементов полукольца выполняется поглощающее свойство нуля: . То есть, в отличие от кольца у нас не требуют обратного элемента по сложению, а требование для нуля вытекает как раз из отсутствия формального минуса. Здесь можно посмотреть примеры структур, где оно не выполняется. Каких-то общих размышлений о полукольце не будет. Просто скажу, что у него есть много практических применений. Например, в этой книжке научно обосновывается (с точки зрения математики) и детально описывается тот факт, что широкий класс задач на графах можно решить более менее унифицированным способом за счёт обобщения матричного умножения и адаптации матрицы инцидентности к нужному умножению. Для этого необходимо рассматривать матрицы не как систему чисел, а как систему произвольных элементов, над которыми можно построить алгебру. То есть рассматривается множество  всех квадратных матриц размера , элементы которой взяты из полукольца . Тогда умножение матриц можно определить в терминах операций полукольца: И, например, в полукольце , решается задача о нахождении размера самого короткого пути между вершинами  и  за  или меньше шагов. В полукольце же  решается аналогичная задача для нахождения размера самого длинного пути.   Проверим для начала, насколько корректно работает возведение в степень в полукольце целых чисел. Действительно, получилась матрица  Теперь рассмотрим следующий граф для примера Как уже говорилось ранее, мы можем решать задачи, обобщая матричное умножение. В случае двух упомянутых ранее задач, всё что надо сделать это вычислить . Например, очевидно, что кратчайший путь   за 3 и менее шагов займёт 8 единиц, а длиннейший за 2 и менее шагов - 13. Надеюсь этот материал принёс вам гораздо больше полезного и интересного, чем предыдущий. Да, многие из нас на работе гоняют json'ы, клепают формочки, но разве это значит, что не нужно развиваться? Мне кажется, порой мы забываем о том, что профессия программиста неразрывно связана с творчеством, поиском, исследованиями и энтузиазмом, который драйвит всю эту деятельность. Всё ещё впереди. Поэтому, хочется закончить словами отсюда. Лучше и не скажешь. Ещё я веду telegram канал StepOne, где оставляю небольшие заметки про разработку и мир IT. Алгебраический код на GitHub Пишу software и статьи