関数型プログラミングや PLT の分野では、特にオブジェクト、コモナド、レンズなどについて議論しているときに、「コアルジェブラ」という用語を何度か耳にしました。この用語を Google で検索すると、これらの構造の数学的説明を示すページが表示されますが、私にはまったく理解できません。プログラミングの文脈でコアルジェブラが何を意味するのか、その重要性は何か、オブジェクトやコモナドとどのように関係するのかを説明していただけませんか?
ベストアンサー1
代数
まず最初に、代数の概念を理解するところから始めるべきだと思います。代数とは、群、環、モノイドなどの代数構造を一般化したものです。ほとんどの場合、これらは集合の観点から紹介されますが、友人同士なので、代わりに Haskell の型についてお話しします。(ギリシャ文字を使わずにはいられません。ギリシャ文字を使うとすべてがかっこよく見えるからです。)
代数とは、τ
いくつかの関数と恒等式を持つ型にすぎません。これらの関数は、型の異なる数の引数を取りτ
、 を生成します。カリー化τ
されていない場合、これらはすべて のように見えます。また、関数の一部に対して特別な動作をする(τ, τ,…, τ) → τ
の要素である「恒等式」を持つこともできます。τ
最も単純な例はモノイドです。モノイドとは、τ
関数mappend ∷ (τ, τ) → τ
と単位元を持つ型のことです。他の例としては、グループ (モノイドと似ていますが、関数が追加されています)、リング、格子などがmzero ∷ τ
あります。invert ∷ τ → τ
すべての関数は 上で動作しますτ
が、異なる引数を持つことができます。これらは と記述できτⁿ → τ
、ここで はのτⁿ
タプルにマップされますn
τ
。このように、恒等式を として考えることができます。ここで は単なる空のタプル です。したがって、代数の考え方を実際に簡略化できます。つまり、いくつかの関数を持つ型にすぎません。τ⁰ → τ
τ⁰
()
代数は、コードの場合と同じように、数学における一般的なパターンを「因数分解」したものです。前述のモノイド、グループ、格子など、興味深いものがすべて同様のパターンに従っていることに気づいた人々は、それを抽象化しました。これを行う利点はプログラミングの場合と同じです。つまり、再利用可能な証明を作成し、特定の種類の推論を容易にします。
F-代数
しかし、因数分解はまだ終わりではありません。これまでに、関数 がたくさんありますτⁿ → τ
。実は、これらすべてを 1 つの関数に結合する巧妙なトリックがあります。特に、モノイドを見てみましょう。 と がありますmappend ∷ (τ, τ) → τ
。mempty ∷ () → τ
これらを、和型 を使って 1 つの関数に変換できますEither
。次のようになります。
op ∷ Monoid τ ⇒ Either (τ, τ) () → τ
op (Left (a, b)) = mappend (a, b)
op (Right ()) = mempty
実際に、この変換を繰り返し使用して、任意の代数に対してすべての関数を 1 つに結合できます。(実際τⁿ → τ
、任意の数の関数に対してこれを行うことができ、任意の に対しても同様に行うことができます。)a → τ
b → τ
a, b,…
これにより、代数を、いくつかの混乱から単一の への単一のτ
関数を持つ型として扱うことができます。モノイドの場合、この混乱は次のようになります。グループ (追加の演算を持つ) の場合、これは次のようになります。構造ごとに異なる型になります。では、これらすべての型に共通するものは何でしょうか。最も明らかなのは、これらはすべて積の合計、つまり代数的データ型であるということです。たとえば、モノイドの場合、任意のモノイド τ で機能するモノイド引数型を作成できます。Either
τ
Either (τ, τ) ()
τ → τ
Either (Either (τ, τ) τ) ()
data MonoidArgument τ = Mappend τ τ -- here τ τ is the same as (τ, τ)
| Mempty -- here we can just leave the () out
グループ、リング、格子、その他すべての可能な構造に対しても同じことを行うことができます。
これらすべての型について他に何が特別なのでしょうか? そうです、それらはすべて ですFunctors
! 例:
instance Functor MonoidArgument where
fmap f (Mappend τ τ) = Mappend (f τ) (f τ)
fmap f Mempty = Mempty
したがって、代数の考え方をさらに一般化することができます。これは、何らかの関数を持つ何らかのτ
型にすぎません。実際、これを型クラスとして記述できます。f τ → τ
f
class Functor f ⇒ Algebra f τ where
op ∷ f τ → τ
これは関数 によって決定されるため、しばしば「F 代数」と呼ばれますF
。型クラスを部分的に適用できれば、 のようなものを定義できますclass Monoid = Algebra MonoidArgument
。
余代数
さて、代数とは何か、そしてそれが通常の代数構造の単なる一般化であるということを、よく理解していただけたと思います。では、F 余代数とは何でしょうか。余代数は、代数の「双対」であることを意味します。つまり、代数を取り、矢印をいくつか反転します。上記の定義には矢印が 1 つしか見当たらないので、それを反転します。
class Functor f ⇒ CoAlgebra f τ where
coop ∷ τ → f τ
以上です!この結論は、少し軽薄に思えるかもしれません(笑)。これは、コジェネバとは何かを説明していますが、それがどのように役立つのか、なぜ私たちが気にするのかについての洞察は実際には提供していません。良い例を 1 つまたは 2 つ見つけたり思いついたりしたら、すぐにそれについて説明します :P。
クラスとオブジェクト
少し調べてみたところ、クラスとオブジェクトを表すためにコアルゲーブラを使用する方法について、良いアイデアが浮かんだと思います。クラス内のオブジェクトのすべての可能な内部状態を含む型があり、クラス自体はオブジェクトのメソッドとプロパティを指定するC
コアルゲーブラです。C
代数の例で示したように、任意の に対してa → τ
やのような関数が多数ある場合、和型 を使ってそれらすべてを 1 つの関数に結合できます。双対の「概念」は、 型の関数の多数を結合することです。これは、和型の双対、つまり積型を使って行うことができます。したがって、上記の 2 つの関数 (および ) を考えると、次のように 1 つの関数を作成できます。b → τ
a, b,…
Either
τ → a
τ → b
f
g
both ∷ τ → (a, b)
both x = (f x, g x)
型は(a, a)
直接的に関数子であるため、F 余代数の概念に確実に適合します。この特定のトリックにより、さまざまな関数 (または OOP の場合はメソッド) を 型の単一の関数にパッケージ化できますτ → f τ
。
型の要素は、オブジェクトの内部C
状態を表します。オブジェクトに読み取り可能なプロパティがある場合は、そのプロパティは状態に依存している必要があります。これを行う最も明白な方法は、それらを の関数にすることです。したがって、長さプロパティ (例) が必要な場合は、関数 を使用します。C
object.length
C → Int
引数を受け取って状態を変更できるメソッドが必要です。これを行うには、すべての引数を受け取って新しい を生成する必要があります。と座標を受け取るメソッドC
を想像してみましょう。次のようになります。setPosition
x
y
object.setPosition(1, 2)
C → ((Int, Int) → C)
ここで重要なパターンは、オブジェクトの「メソッド」と「プロパティ」がオブジェクト自体を最初の引数として受け取ることです。これは、self
Python のパラメータや他の多くの言語の暗黙のパラメータとまったく同じですthis
。coalgebra は基本的に、self
パラメータを受け取る動作をカプセル化します。これが最初の引数C
ですC → F C
。
position
それでは、すべてをまとめてみましょう。プロパティ、name
プロパティ、関数を持つクラスを想像してみましょうsetPosition
。
class C
private
x, y : Int
_name : String
public
name : String
position : (Int, Int)
setPosition : (Int, Int) → C
このクラスを表すには 2 つの部分が必要です。まず、オブジェクトの内部状態を表す必要があります。この場合、 2 つInt
の と 1 つのが保持されるだけですString
(これが型 ですC
)。次に、クラスを表す余代数を作成する必要があります。
data C = Obj { x, y ∷ Int
, _name ∷ String }
記述するプロパティは 2 つあります。これらは非常に簡単です。
position ∷ C → (Int, Int)
position self = (x self, y self)
name ∷ C → String
name self = _name self
ここで、位置を更新できるようにする必要があります。
setPosition ∷ C → (Int, Int) → C
setPosition self (newX, newY) = self { x = newX, y = newY }
これは、明示的な変数を持つ Python クラスのようなものですself
。これで関数が多数用意できたのでself →
、それらを 1 つの関数に結合して coalgebra を作成する必要があります。これは、単純なタプルで行うことができます。
coop ∷ C → ((Int, Int), String, (Int, Int) → C)
coop self = (position self, name self, setPosition self)
型((Int, Int), String, (Int, Int) → c)
—for any c
— は関数なので、coop
必要な形式を持ちます: Functor f ⇒ C → f C
。
これを踏まえて、C
上coop
で示したクラスを指定するコアルゴリズムを作成します。同じ手法を使用して、オブジェクトが持つ任意の数のメソッドとプロパティを指定できることがわかります。
これにより、クラスを扱うために余代数的推論を使用することができます。たとえば、「F 余代数準同型」という概念を導入して、クラス間の変換を表すことができます。これは、構造を保持する余代数間の変換を意味する、恐ろしい響きの用語です。これにより、クラスを他のクラスにマッピングすることについて考えることがはるかに簡単になります。
つまり、F 余代数は、self
各オブジェクトの内部状態を含むパラメータに依存する一連のプロパティとメソッドを持つことでクラスを表します。
その他のカテゴリー
これまで、Haskell の型としての代数と余代数について説明してきました。代数は単なる関数τ
を持つ型でありf τ → τ
、余代数は単なる関数を持つ型τ
ですτ → f τ
。
しかし、これらのアイデアは Haskell自体に特に結びつくものではありません。実際、これらは通常、型や Haskell 関数ではなく、集合や数学関数の観点から導入されます。実際、これらの概念はあらゆるカテゴリに一般化できます。
あるカテゴリ に対して F 代数を定義できますC
。まず、関数F : C → C
、つまり自己関数が必要です。(すべての HaskellFunctor
は、実際には からの自己関数ですHask → Hask
。) 次に、代数は、射 を持つA
からのオブジェクトにすぎません。余代数は、 を除いて同じです。C
F A → A
A → F A
他のカテゴリを考慮することで何が得られるのでしょうか? そうですね、同じアイデアをさまざまなコンテキストで使用できます。たとえば、モナドです。Haskell では、モナドはM ∷ ★ → ★
3 つの演算を持つ型です。
map ∷ (α → β) → (M α → M β)
return ∷ α → M α
join ∷ M (M α) → M α
関数は、が であるmap
という事実の単なる証明です。したがって、モナドはと という2 つの演算を持つ関数であると言えます。M
Functor
return
join
関数はそれ自体がカテゴリを形成し、関数間の射影はいわゆる「自然変換」です。自然変換とは、ある関数をその構造を維持しながら別の関数に変換する方法にすぎません。こちらはアイデアを説明するのに役立つ素晴らしい記事です。リスト専用のconcat
について説明しています。join
Haskell の関数では、2 つの関数の合成自体が関数になります。疑似コードでは、次のように記述できます。
instance (Functor f, Functor g) ⇒ Functor (f ∘ g) where
fmap fun x = fmap (fmap fun) x
join
これは、 を からのマッピングとして考えるのに役立ちますf ∘ f → f
。 の型join
は です。直感的に、すべての型∀α. f (f α) → f α
に有効な関数がの変換として考えられることがわかります。α
f
return
は同様の変換です。その型は です∀α. α → f α
。これは異なっているように見えます。最初のものα
は関数「内」にありません。幸いなことに、恒等関数を追加することでこれを修正できます。∀α. Identity α → f α
変換 も同様return
ですIdentity → f
。
これで、モナドを、f
演算 および を伴う関数に基づく単なる代数として考えることができますf ∘ f → f
。これは見覚えがありませんか? これは、演算 および を伴う単なるIdentity → f
型であるモノイドに非常に似ています。τ
τ × τ → τ
() → τ
つまり、モナドはモノイドとまったく同じですが、型の代わりに関数子があります。これは同じ種類の代数ですが、カテゴリが異なります。(私の知る限り、「モナドは自己関数子のカテゴリ内のモノイドにすぎない」というフレーズはここから来ています。)
f ∘ f → f
これで、とという 2 つの演算ができましたIdentity → f
。対応する余代数を得るには、矢印を反転するだけです。これにより、f → f ∘ f
とという 2 つの新しい演算が得られます。f → Identity
上記のように型変数を追加して、これらを Haskell 型に変換し、∀α. f α → f (f α)
と を得ることができます∀α. f α → α
。これは、コモナドの定義とまったく同じです。
class Functor f ⇒ Comonad f where
coreturn ∷ f α → α
cojoin ∷ f α → f (f α)
したがって、コモナドは、自己関数のカテゴリ内の余代数です。