scalabook

Форк
0
316 строк · 14.4 Кб

Моноид

Формальное определение

Моноид (monoid) — это полугруппа с единичным (нейтральным) элементом.

\((M, +)\) является моноидом для заданного множества \(M\) и операции \(+\), если удовлетворяет следующим свойствам для любых \(x, y, z \in M\):

  • Замыкание (closure): для \(\forall x, y \in M\) выполняется \(x + y \in M\).
  • Ассоциативность (associativity): для \(\forall x, y, z \in M\) выполняется \((x + y) + z = x + (y + z)\).
  • Тождественность или существования нейтрального элемента (identity): существует \(\exists e \in M\) такой, что для \(\forall x \in M\) выполняется \(e + x = x + e = x\)

Первые два закона наследуются от полугруппы.

Также говорится, что M — моноид относительно +.

В любом моноиде имеется ровно один нейтральный элемент.

Используя определение натуральной степени элемента полугруппы ("сложить n раз") как: \(a^{n} = \overset{n}{a + ... + a}\), можно расширить определение до 0 степени: \(a^{0} = e\).

Определение из моноидальной категории

Моноид в моноидальной категории — это объект \(m\), снабженный двумя морфизмами:

  • \(\mu : m \bigotimes m \to m\)
  • \(\eta : I \to m\)

удовлетворяющих законам единицы и ассоциативности:

  • \(id_{m} \bigotimes m = m \bigotimes id_{m} = m\)
  • \((m \bigotimes m) \bigotimes m = m \bigotimes (m \bigotimes m)\)

Определение в виде кода на Scala

trait Monoid[A] extends Semigroup[A]:
def empty: A
def combineNonNegN(x: A, n: Int :| GreaterEqual[0]): A =
if n == 0 then empty
else
val pos: Int :| Greater[0] = n.refine
combineN(x, pos)

О том, что такое Int :| GreaterEqual[0]

рассказывается в статье об уточняющих типах.

Для операции сложения встречаются наименования, перечисленные в Полугруппе. Для единичного элемента встречаются следующие наименования: empty

, mempty
(Haskell), id
.

Законы в виде кода на Scala

Законы моноида принимают следующий вид:

  • замыкание следует из определения функции combine
    - тип результата тот же, что и тип переменных
  • ассоциативность формулируется так: m.combine(x, m.combine(y, z)) == m.combine(m.combine(x, y), z)
  • тождественность формулируется так: (m.combine(x, m.empty) == x) && (m.combine(m.empty, x) == x)
trait MonoidLaw extends SemigroupLaw:
def checkMonoidLaw[A: Monoid](x: A, y: A, z: A): ValidatedNel[String, Unit] =
checkSemigroupLaw(x, y, z) combine
check(
Monoid[A].combine(x, Monoid[A].empty) == x,
"right identity: x + e = x"
) combine
check(
Monoid[A].combine(Monoid[A].empty, x) == x,
"left identity: e + x = x"
)

Законы полугруппы (ассоциативность) определяются в родительском SemigroupLaw.

Примеры

Числа относительно сложения с 0

Int

относительно сложения с 0
образуют моноид, потому что:

  • сложение ассоциативно: (a + b) + c = a + (b + c)
  • 0
    - это единичный элемент относительно сложения: 0 + a = a + 0 = a
given sumMonoidInstance: Monoid[Int] with
val empty = 0
def combine(x: Int, y: Int): Int = x + y

Числа относительно умножения с 1

Int

относительно умножения с единичным элементом 1
образуют моноид, потому что:

  • умножение ассоциативно: (a * b) * c = a * (b * c)
  • 1
    - это единичный элемент относительно сложения: 1*a = a*1 = a
given productMonoidInstance: Monoid[Int] with
val empty = 1
def combine(x: Int, y: Int): Int = x * y

Строки

String

относительно конкатенации строк с единичным элементом пустой строкой ""
образуют моноид, потому что:

  • конкатенация строк ассоциативна: (a + b) + c = a + (b + c)
    • "мама" + ("мыла" + "раму") = ("мама" + "мыла") + "раму" = "мамамылараму"
  • ""
    - это единичный элемент относительно конкатенации: "" + a = a + "" = a
given stringMonoidInstance: Monoid[String] with
val empty = ""
def combine(x: String, y: String): String = x + y

Последовательность

given listMonoidInstance[T]: Monoid[List[T]] with
val empty = List.empty[T]
def combine(x: List[T], y: List[T]): List[T] = x ++ y

Кортеж от двух и более моноидов

given nestedMonoidInstance[A, B](using aMonoid: Monoid[A], bMonoid: Monoid[B]): Monoid[(A, B)] with
lazy val empty: (A, B) = (aMonoid.empty, bMonoid.empty)
def combine(x: (A, B), y: (A, B)): (A, B) = (aMonoid.combine(x._1, y._1), bMonoid.combine(x._2, y._2))

Option

Option

является моноидом, если его параметр типа - полугруппа, например:

given optionMonoidInstance[A: Semigroup]: Monoid[Option[A]] with
val empty: Option[A] = None
def combine(x: Option[A], y: Option[A]): Option[A] =
(x, y) match
case (Some(a1), Some(a2)) => Some(summon[Semigroup[A]].combine(a1, a2))
case (Some(_), None) => x
case (None, Some(_)) => y
case (None, None) => None

Множество - Boolean
, операция - логическое И (&&
), пустой элемент - true

given booleanAnd: Monoid[Boolean] with
def combine(a1: Boolean, a2: Boolean): Boolean = a1 && a2
val empty: Boolean = true

Множество - Boolean
, операция - логическое Или (||
), пустой элемент - false

given booleanOr: Monoid[Boolean] with
def combine(a1: Boolean, a2: Boolean): Boolean = a1 || a2
val empty: Boolean = false

Использование моноидов

Моноиды тесно связаны со списками. Например, вот как можно свернуть список, используя моноид:

def combineAll[A](as: List[A], m: Monoid[A]): A =
as.foldLeft(m.empty)(m.combine)
combineAll(List(1, 2, 3, 4), intMultiplication)
// res4: Int = 24

Тот факт, что операция combine

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

В качестве примера предположим, что есть последовательность a

, b
, c
, d
, которую необходимо сократить, используя некоторый моноид. Balanced fold выглядит так:

combine(combine(a, b), combine(c, d))

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

Например, рассмотрим производительность этого выражения во время выполнения:

List("lorem", "ipsum", "dolor", "sit").foldLeft("")(_ + _)

На каждом этапе сворачивания (fold) выделяется полная промежуточная строка только лишь для того, чтобы быть отброшенной, а String

выделяет более крупную строку на следующем шаге. Напомним, что значения являются неизменяемыми, и что String
вычисляется для строк и требует выделения нового массива символов и копирования a + b
для строк a
и b
в этот новый массив. Это занимает время, пропорциональное a.length + b.length
.

Вот трассировка вычисляемого предыдущего выражения:

List("lorem", "ipsum", "dolor", "sit").foldLeft("")(_ + _)
List("ipsum", "dolor", "sit").foldLeft("lorem")(_ + _)
List("dolor", "sit").foldLeft("loremipsum")(_ + _)
List("sit").foldLeft("loremipsumdolor")(_ + _)
List().foldLeft("loremipsumdolorsit")(_ + _)
"loremipsumdolorsit"

Обратите внимание, что промежуточные строки создаются, а затем сразу же отбрасываются. Более эффективной стратегией было бы объединение половинок последовательности, что называется сбалансированным сворачиванием (balanced fold) — сначала строятся "loremipsum"

и "dolorsit"
, а затем они складываются вместе.

Операции с моноидами

Моноиды можно объединять: если A

и B
- это моноиды, то кортеж (A, B)
также является моноидом (и называется продуктом)

given productMonoid[A, B](using ma: Monoid[A], mb: Monoid[B]): Monoid[(A, B)] with
def combine(x: (A, B), y: (A, B)): (A, B) =
(ma.combine(x(0), y(0)), mb.combine(x(1), y(1)))
val empty: (A, B) = (ma.empty, mb.empty)

Моноидом также является Map

, если доступен моноид для его значений:

given mapMergeMonoid[K, V](using mv: Monoid[V]): Monoid[Map[K, V]] with
def combine(a: Map[K, V], b: Map[K, V]): Map[K, V] =
(a.keySet ++ b.keySet).foldLeft(empty) { (acc, k) =>
acc.updated(k, mv.combine(a.getOrElse(k, mv.empty), b.getOrElse(k, mv.empty)))
}
val empty: Map[K, V] = Map()

Моноидом является функция, результаты которой являются моноидами.

given functionMonoid[A, B](using mb: Monoid[B]): Monoid[A => B] with
def combine(f: A => B, g: A => B): A => B = a => mb.combine(f(a), g(a))
val empty: A => B = _ => mb.empty

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

val p = List(1, 2, 3, 4).foldMap(a => (1, a))
// p: Tuple2[Int, Int] = (4, 10)
val mean = p(0) / p(1).toDouble
// mean: Double = 0.4

Реализация

Реализация в Cats

import cats.*
import cats.implicits.*
Monoid[Int].combine(1, Monoid[Int].empty) // 1
"Hi " |+| "there" |+| Monoid[String].empty // Hi there

Реализация в ScalaZ

import scalaz.*
import Scalaz.*
mzero[List[Int]] // List()

Реализация в Spire

1 |+| Monoid.empty[Int] // 1
Monoid.isEmpty(Monoid.empty[Int]) // true

Ссылки:

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.