monoid

id:akihiro4chawon 氏の 比較はモノイド を読んで新しい視点が得られた気がするので忘れないうちに書いておく。

半群とは

Wikipedia から引用

演算が閉じている

  • S の各元 a, b に対して、演算結果 a • b は再び S に属する。

結合律

  • S の各元 a, b, c に対して、等式 (a • b) • c = a • (b • c) が満たされる。

の2つの条件を満たせば半群 (semigroup) らしいので Groovy で書いてみる。

boolean isSemigroup(Set S, Closure op) {
  // 2項演算子であること
  assert op.maximumNumberOfParameters == 2
  isClosed(S, op) && isAssociative(S, op)
}

// 演算が閉じていることを調べる
boolean isClosed(Set S, Closure op) {
  for (a in S)
    for (b in S)
      if (!(op(a, b) in S)) return false
  return true
}

// 結合律を調べる
boolean isAssociative(Set S, Closure op) {
  for (a in S)
    for (b in S)
      for (c in S)
        if (op(a, op(b, c)) != op(op(a, b), c)) return false
  return true
}

// ?: は
assert isSemigroup([-1,0,1] as Set, { a, b -> a ?: b })

Groovy の ?: 演算子と比較の結果の集合 [-1,0,1] は半群であることを確認できる。

半群を探す

他にも条件を満たす演算子があるような気がしてきたので Groovy の二項演算子を全部調べてみる。

def operators() {
  [
    '?:':{ a, b -> a ?: b },
    '+' :{ a, b -> a + b },
    '-' :{ a, b -> a - b },
    '*' :{ a, b -> a * b },
    '**':{ a, b -> a ** b },
    '/' :{ a, b -> a / b },
    'intdiv':{ a, b -> a.intdiv(b) },
    '%' :{ a, b -> a % b },
    '|' :{ a, b -> a | b },
    '&' :{ a, b -> a & b },
    '^' :{ a, b -> a ^ b },
    '<<':{ a, b -> a << b },
    '>>':{ a, b -> a >> b },
    '>>>':{ a, b -> a >> b },
    '==':{ a, b -> a == b },
    '!=':{ a, b -> a != b },
    '<=>':{ a, b -> a <=> b },
    '>' :{ a, b -> a > b },
    '>=':{ a, b -> a >= b },
    '<' :{ a, b -> a < b },
    '<=':{ a, b -> a <= b },
  ]
}

def semigroupOps(Set S) {
  operators().findAll { name, op ->
    try {
      isSemigroup(S, op)
    } catch(e) {
      false
    }
  }
}
assert semigroupOps([-1,0,1] as Set)*.key == ['?:', '*', '|', '&']

4つの演算子が条件を満たしているようだ。

モノイドを探す

Wikipedia から引用。

モノイドは単位元をもつ半群(単位的半群)である

単位元の存在

  • S の元 e が存在して、S の任意の元 a に対して e • a = a • e = a なる等式が成り立つ。

結合律は半群で条件を満たすので単位元の条件を追加すればモノイドということになる。

boolean isMonoid(Set S, Closure op) {
  isSemigroup(S, op) && hasIdentity(S, op)
}

boolean hasIdentity(Set S, Closure op) {
  for (e in S)
    if (S.every { a -> op(a, e) == a && op(e, a) == a })
      return true
  return false
}

def monoidOps(Set S) {
  operators().findAll { name, op ->
    try {
      isMonoid(S, op)
    } catch(e) {
      false
    }
  }
}
assert monoidOps([-1,0,1] as Set)*.key == ['?:', '*', '|', '&']

先程の4つの演算子と比較結果の集合はモノイドの条件も満たしていた。

def identity(Set S, Closure op) {
  for (e in S)
    if (S.every { a -> op(a, e) == a && op(e, a) == a })
      return e
  assert false
}

assert identity([-1,0,1] as Set, { a, b -> a ?: b }) == 0
assert identity([-1,0,1] as Set, { a, b -> a * b })  == 1
assert identity([-1,0,1] as Set, { a, b -> a | b })  == 0
assert identity([-1,0,1] as Set, { a, b -> a & b })  == -1

単位元演算子によって変わる。

比較がモノイドであることを確認する

4つの演算子で比較してみる。

([-1,0,1] as Set).with { S ->
  def to_s = [(-1):'LT',(0):'EQ',(1):'GT']
  monoidOps(S).each { name, op ->
    println "A $name B"
    for (a in S)
      for (b in S)
        println "${to_s[a]} $name ${to_s[b]} = ${to_s[op(a, b)]}, ${to_s[-op(-a, -b)]}"
  }
}


結果

A ?: B
EQ ?: EQ = EQ, EQ
EQ ?: GT = GT, GT
EQ ?: LT = LT, LT
GT ?: EQ = GT, GT
GT ?: GT = GT, GT
GT ?: LT = GT, GT
LT ?: EQ = LT, LT
LT ?: GT = LT, LT
LT ?: LT = LT, LT
A * B
EQ * EQ = EQ, EQ
EQ * GT = EQ, EQ
EQ * LT = EQ, EQ
GT * EQ = EQ, EQ
GT * GT = GT, LT
GT * LT = LT, GT
LT * EQ = EQ, EQ
LT * GT = LT, GT
LT * LT = GT, LT
A | B
EQ | EQ = EQ, EQ
EQ | GT = GT, GT
EQ | LT = LT, LT
GT | EQ = GT, GT
GT | GT = GT, GT
GT | LT = LT, GT
LT | EQ = LT, LT
LT | GT = LT, GT
LT | LT = LT, LT
A & B
EQ & EQ = EQ, EQ
EQ & GT = EQ, EQ
EQ & LT = EQ, EQ
GT & EQ = EQ, EQ
GT & GT = GT, GT
GT & LT = GT, LT
LT & EQ = EQ, EQ
LT & GT = GT, LT
LT & LT = LT, LT

カンマの右は Comparator#compare の条件を満たしている場合は同じになるように設定した。
Javadoc から引用。

実装では、すべての x と y に対して sgn(compare(x, y)) == -sgn(compare(y, x)) が保証されなければいけません。

残念ながら compare で使える演算子は ?: だけだった。
比較はモノイドであるが、モノイドであれば比較で使えるわけではないということみたいだ。
でもモノイドであることは何か有用な気がしてきたので気になることを確認する。

結合律を確認する

つまり foldLeft と foldRight のどちらで計算しても結果は同じということ。

// すべての要素が集合 S に属するサイズ n のリストを返す
def randomList(Set S, int n) {
  def list = S as List
  (1..n).collect { list[Math.random() * list.size() as int] }
}

// 半群であることを利用する
([-1,0,1] as Set).with { S ->
  def l = randomList(S, 5)
  def r = l.reverse()
  semigroupOps(S).each { name, op ->
    assert l.tail().inject(l.head()) { a, b -> op(a,b) } == r.tail().inject(r.head()) { b, a -> op(a,b) } : "A $name B"
  }
}

// モノイドであることを利用する
([-1,0,1] as Set).with { S ->
  def l = randomList(S, 5)
  def r = l.reverse()
  monoidOps(S).each { name, op ->
    def e = identity(S, op)
    assert l.inject(e) { a, b -> op(a,b) } == r.inject(e) { b, a -> op(a,b) } : "A $name B"
  }
}

使ってみる

Groovy のリストは Comparable ではないので List のための Comparator を実装する。
結合律があるのでどちら側から計算しても結果は同じ。

// 再帰で比較
def asc1 = { List l1, List l2, int i = 0 ->
  l1.size() < i && l2.size() < i ? 0 : l1[i] <=> l2[i] ?: trampoline(l1, l2, i+1)
}.trampoline()

// 左結合で比較
def asc2 = { List l1, List l2 ->
  [l1, l2].transpose().inject(0) { acc, pair -> acc ?: pair[0] <=> pair[1] } ?: l1.size() <=> l2.size()
}

// 右結合で比較
def asc3 = { List l1, List l2 ->
  [l1, l2].transpose().reverse().inject(0) { acc, pair -> pair[0] <=> pair[1] ?: acc } ?: l1.size() <=> l2.size()
}

def lists = (1..5).collect { randomList(0..9 as Set, 5) }
println lists
assert lists.clone().sort(asc2) == lists.clone().sort(asc1)
assert lists.clone().sort(asc3) == lists.clone().sort(asc1)
println lists.clone().sort(asc1)

右結合を使えるということは

foldr といえば無限リスト、Merge が合言葉。
Haskell で遅延評価を使って fold で無限リストを比較してみる。
サイズの比較部分は省略した。

order EQ a  = a
order LT _  = LT
order GT _  = GT

compareR :: (Ord a) => [a] -> [a] -> Ordering
compareR xs ys = foldr (order . (uncurry compare)) EQ $ zip xs ys

compareL :: (Ord a) => [a] -> [a] -> Ordering
compareL xs ys = foldl (flip (order . (uncurry compare))) EQ $ zip xs ys


結果

*Main> compareR [1..] [1,1..]
GT

*Main> compareL [1..] [1,1..]
<interactive>: out of memory

Haskell では compare で無限リストも比較できるがその実装は再帰によるパターンマッチだった。
でも比較がモノイドであるので foldr でも実装できることが分かった。
以前、フィボナッチ数列でやった行列計算とかもモノイド?という風にいろいろモノイドに見えてきた。

まとめ

  • 比較はモノイドである
  • モノイドであっても比較とは限らない
  • モノイドであることに気づければ foldr に持ち込める