特にオブジェクト、コモナ、レンズなどについての議論の場合、関数型プログラミングやPLTサークルで「代数」という用語を何度か聞いたことがあります。この用語をグーグルで検索すると、これらの構造の数学的な説明を提供するページが提供されますが、これは私にはほとんど理解できません。誰もがプログラミングの文脈での代数が何を意味するのか、それらの重要性は何なのか、それらはオブジェクトやコマンドにどのように関係するのかを説明できますか?
始める場所は、代数の概念を理解することだと思います。これは、グループ、リング、モノイドなどの代数構造の一般化です。ほとんどの場合、これらのことはセットの観点から紹介されますが、私たちは友人であるため、代わりにHaskellのタイプについて説明します。 (ただし、ギリシャ文字を使用することは避けられません。すべての文字がクールに見えます!)
したがって、代数は、いくつかの関数とアイデンティティーを持つτ
型にすぎません。これらの関数は、τ
型の異なる数の引数を取り、τ
を生成します。カリー化されていないため、すべて(τ, τ,…, τ) → τ
のように見えます。また、いくつかの関数で特別な動作をするτ
の要素である「アイデンティティ」を持つこともできます。
これの最も簡単な例はモノイドです。モノイドは、任意のタイプτ
であり、関数mappend ∷ (τ, τ) → τ
とID mzero ∷ τ
を持ちます。他の例には、グループ(余分なinvert ∷ τ → τ
関数を除いてモノイドに似ています)、リング、ラティスなどが含まれます。
すべての関数はτ
で動作しますが、異なるアリティを持つことができます。これらをτⁿ → τ
として書き出すことができます。ここで、τⁿ
はn
τ
のタプルにマップされます。このように、アイデンティティをτ⁰ → τ
として考えるのは理にかなっています。ここで、τ⁰
は単なる空のTuple ()
です。したがって、代数の概念を実際に単純化することができます。それは、いくつかの関数を備えた単なる型です。
代数は、コードで行うように、「ファクタリング」された数学の一般的なパターンです。人々は、前述のモノイド、グループ、ラティスなどの興味深いものがすべて同じようなパターンに従っていることに気づいたので、それを抽象化しました。これを行う利点は、プログラミングの場合と同じです。再利用可能な証明を作成し、特定の種類の推論を容易にします。
ただし、ファクタリングはまだ完了していません。これまでのところ、たくさんの関数τⁿ → τ
があります。これらをすべて1つの関数に結合するために、実際に巧妙なトリックを行うことができます。特に、モノイドを見てみましょう。mappend ∷ (τ, τ) → τ
とmempty ∷ () → τ
があります。合計タイプEither
を使用して、これらを単一の関数に変換できます。次のようになります。
op ∷ Monoid τ ⇒ Either (τ, τ) () → τ
op (Left (a, b)) = mappend (a, b)
op (Right ()) = mempty
実際にこの変換を繰り返し使用して、allτⁿ → τ
関数をany代数の単一の関数に結合できます。 (実際、これはanya → τ
に対して任意の数の関数b → τ
、a, b,…
などに対して行うことができます。)
これにより、Either
sのいくつかの混乱から単一のτ
へのsingle関数を持つタイプτ
として代数について話すことができます。モノイドの場合、この混乱はEither (τ, τ) ()
;です。グループ(余分なτ → τ
操作がある)の場合、Either (Either (τ, τ) τ) ()
です。構造ごとに異なるタイプです。では、これらすべてのタイプに共通するものは何ですか?最も明白なことは、それらはすべて積の単なる和、つまり代数データ型であるということです。たとえば、モノイドの場合、any monoidτで機能するモノイド引数型を作成できます。
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 τ
そして、それだけです!さて、この結論は少し気味が悪いように見えるかもしれません(へ)。それはwhatが代数であるということを伝えますが、それがどのように有用であるか、なぜ私たちが気にするのかについての洞察を実際には与えません。良い例が1つまたは2つ見つかったら、またはその2つを思いついたら、少し後で説明します。
少し読んだ後、クラスとオブジェクトを表現するために、代数を使用する方法について良いアイデアを持っていると思います。クラス内のオブジェクトの考えられる内部状態をすべて含むC
型があります。クラス自体は、オブジェクトのメソッドとプロパティを指定するC
の上の代数です。
代数の例に示されているように、a → τ
に対してb → τ
やa, b,…
のような関数の束がある場合、Either
を使用してすべてを1つの関数に結合できます。合計タイプ。二重の「概念」は、タイプτ → a
、τ → b
などの一連の関数を組み合わせたものです。これを行うには、双対の合計タイプ(製品タイプ)を使用します。したがって、上記の2つの関数(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)
のようになります。
ここで重要なパターンは、オブジェクトの「メソッド」と「プロパティ」がオブジェクト自体を最初の引数として使用することです。これは、Pythonのself
パラメーターと、他の多くの言語の暗黙的なthis
と同じです。コアジェブラは、基本的にself
パラメーターを取得する動作をカプセル化するだけです。これは、C → F C
の最初の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
sと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 }
これは、明示的なself
変数を持つPythonクラスに似ています。 self →
関数の束ができたので、これらを結合して代数の単一の関数にする必要があります。簡単なタプルでこれを行うことができます。
coop ∷ C → ((Int, Int), String, (Int, Int) → C)
coop self = (position self, name self, setPosition self)
タイプ((Int, Int), String, (Int, Int) → c)
——anyc
—はファンクターであるため、coop
にはFunctor f ⇒ C → f C
という形式があります。
これを考えると、C
はcoop
と一緒に、上で与えたクラスを指定する合算を形成します。この同じ手法を使用して、オブジェクトに必要なメソッドとプロパティをいくつでも指定する方法を確認できます。
これにより、代数的推論を使用してクラスを処理できます。たとえば、クラス間の変換を表す「F代数準同型」の概念を取り入れることができます。これは恐ろしく聞こえる用語で、構造を保存する炭代間の変換を意味します。これにより、クラスを他のクラスにマッピングすることを非常に簡単に考えることができます。
要するに、F代数は、すべてのオブジェクトの内部状態を含むself
パラメーターにすべて依存する一連のプロパティとメソッドを持つことにより、クラスを表します。
これまで、Haskell型としての代数と代数について説明してきました。代数は関数τ
を持つf τ → τ
型だけであり、代数は関数τ
を持つτ → f τ
型だけです。
しかし、これらのアイデアをHaskellと実際に結び付けるものはありませんper se。実際、それらは通常、タイプとHaskell関数ではなく、セットと数学関数の観点から導入されています。実際、これらの概念をanyカテゴリーに一般化できます!
いくつかのカテゴリC
に対してF代数を定義できます。まず、ファンクターF : C → C
、つまりendofunctorが必要です。 (すべてのHaskell Functor
sは、実際にはHask → Hask
からの内在関数です。)それから、代数は、オブジェクトA
からC
で、射影F A → A
を持ちます。代数はA → F A
を除いて同じです。
他のカテゴリを考慮することで何が得られますか?まあ、異なるコンテキストで同じアイデアを使用できます。モナドのように。 Haskellでは、モナドは3つの操作を持つM ∷ ★ → ★
型です:
map ∷ (α → β) → (M α → M β)
return ∷ α → M α
join ∷ M (M α) → M α
map
関数は、M
がFunctor
であるという事実の単なる証明です。そのため、モナドは、two操作を伴う単なるファンクターであると言えます:return
およびjoin
。
ファンクター自体がカテゴリーを形成し、それらの間の射はいわゆる「自然変換」です。自然な変換は、その構造を維持しながら1つのファンクターを別のファンクターに変換する方法にすぎません。 こちら アイデアの説明に役立つ素敵な記事。 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 α
です。直感的に、all types α
に有効な関数がf
の変換と考えることができる方法を見ることができます。
return
も同様の変換です。タイプは∀α. α → f α
です。これは異なって見えます-最初のα
はファンクターに「入っていません」!幸いなことに、IDファンクタを追加することでこれを修正できます:∀α. Identity α → f α
。 return
は変換Identity → f
です。
これで、モナドを、いくつかのファンクターf
を基にした演算f ∘ f → f
およびIdentity → f
に基づいた単なる代数と考えることができます。これはおなじみに見えませんか?これはモノイドと非常によく似ています。モノイドは、操作τ
およびτ × τ → τ
を備えた() → τ
型にすぎません。
したがって、モナドはモノイドに似ていますが、タイプを持つ代わりにファンクターがあります。これは同じ種類の代数であり、異なるカテゴリーに属しています。 (これは、私が知る限り、「モナドはエンドファンクターのカテゴリーのモノイドである」というフレーズの由来です。)
これで、f ∘ f → f
とIdentity → f
の2つの操作ができました。対応する代数を取得するには、矢印を反転します。これにより、2つの新しい操作f → f ∘ f
とf → Identity
が得られます。上記のように型変数を追加して、∀α. f α → f (f α)
と∀α. f α → α
を与えることで、それらをHaskell型に変えることができます。これは、コモナの定義とまったく同じです。
class Functor f ⇒ Comonad f where
coreturn ∷ f α → α
cojoin ∷ f α → f (f α)
したがって、共関数は、内積関数のカテゴリではcoalgebraになります。
チュートリアルペーパー(co)代数と(co)帰納法に関するチュートリアルを読むと、コンピューターサイエンスの共同代数についての洞察が得られるはずです。
以下は、あなたを納得させるための引用です。
一般的に、あるプログラミング言語のプログラムはデータを操作します。過去数十年にわたるコンピューターサイエンスの発展の中で、たとえば、プログラムが動作するデータの特定の表現に依存しないようにするために、これらのデータの抽象的な記述が望ましいことが明らかになりました。また、このような抽象性は、正当性の証明を容易にします。
この欲求は、代数的仕様または抽象データ型理論と呼ばれる分岐で、コンピューターサイエンスで代数的手法を使用するようになりました。調査の対象は、代数からよく知られている手法の概念を使用した、それ自体がデータ型です。コンピュータ科学者が使用するデータ型は、多くの場合、指定された(コンストラクタ)操作のコレクションから生成されます。このため、代数の「初期性」が重要な役割を果たします。
標準的な代数的手法は、コンピューターサイエンスで使用されるデータ構造のさまざまな重要な側面をキャプチャするのに役立つことがわかっています。しかし、コンピューティングで発生する本質的に動的な構造の一部を代数的に記述することは困難であることが判明しました。このような構造には通常、状態の概念が含まれ、さまざまな方法で変換できます。そのような状態ベースの動的システムへの形式的なアプローチは、一般に、古典的な初期の参照として、オートマトンまたは遷移システムを利用します。
この10年の間に、そのような状態ベースのシステムは代数としてではなく、いわゆる共代数として記述されるべきであるという洞察が徐々に高まりました。これらは、このチュートリアルで正確になるように、代数の正式な双対です。代数の「初期性」の二重の性質、すなわち最終性は、そのような共代数にとって重要であることが判明しました。そして、そのような最終的な共代数に必要な論理的推論の原理は帰納ではなく、共帰納です。
プレリュード、カテゴリー理論についてカテゴリー理論は、ファンクターの理論の名前を変更する必要があります。カテゴリはファンクターを定義するために定義する必要があるものです。 (さらに、ファンクターは、自然な変換を定義するために定義する必要があるものです。)
ファンクタとはこれは、あるセットから別のセットへの変換であり、その構造を保持します。 (詳細については、ネット上で多くの良い説明があります)。
F代数とは?これはファンクターの代数です。これは、ファンクターの普遍的妥当性の研究に過ぎません。
どのようにコンピューターサイエンスにリンクできますか?プログラムは、構造化された一連の情報として見ることができます。プログラムの実行は、この構造化された情報セットの変更に対応します。実行はプログラム構造を保存するのが良いと思います。実行は、この情報セットに対するファンクターのアプリケーションとして表示できます。 (プログラムを定義するもの)。
なぜF-co-代数?プログラムは、情報によって記述され、それに基づいて行動するため、本質的に二重です。次に、主にプログラムを構成して変更する情報を2つの方法で表示できます。
そして、この段階で、私はそれを言いたいです、
プログラムの存続期間中に、データと状態が共存し、それらが互いに完了します。彼らは二重です。
明らかにプログラミングに関連するものから始めてから、数学的なものを追加して、できる限り具体的で現実的なものにします。
http://www.cs.umd.edu/~micinski/posts/2012-09-04-on-understanding-coinduction.html
帰納は有限データに関するものであり、共帰納は無限データに関するものです。
無限データの典型的な例は、遅延リスト(ストリーム)のタイプです。たとえば、メモリに次のオブジェクトがあるとしましょう。
let (pi : int list) = (* some function which computes the digits of
π. *)
コンピュータは有限のメモリしか持っていないため、πのすべてを保持することはできません!しかし、それができることは有限のプログラムを保持することです。それはあなたが望む任意の長さのπの拡張を生成します。リストの有限部分のみを使用している限り、その無限リストで必要なだけ計算できます。
ただし、次のプログラムを検討してください。
let print_third_element (k : int list) = match k with
| _ :: _ :: thd :: tl -> print thd
print_third_element pi
このプログラムは、piの3桁目を出力します。ただし、一部の言語では、関数への引数はすべて、関数に渡される前に評価されます(遅延評価ではなく厳密評価)。この縮小順序を使用すると、上記のプログラムはpiの数字を計算して永久に実行されてから、プリンター関数に渡されます(決して発生しません)。マシンには無限のメモリがないため、プログラムは最終的にメモリ不足になり、クラッシュします。これは最良の評価順序ではない場合があります。
http://adam.chlipala.net/cpdt/html/Coinductive.html
Haskellなどの遅延関数型プログラミング言語では、無限のデータ構造がいたるところにあります。無限リストとよりエキゾチックなデータ型は、プログラムの部分間の通信に便利な抽象化を提供します。無限の遅延構造なしで同様の利便性を実現するには、多くの場合、制御フローのアクロバティックな反転が必要になります。
http://www.alexandrasilva.org/#/talks.html
代数構造は一般に次のようになります。
これは、1。プロパティと2.メソッドを持つオブジェクトのように聞こえます。または、さらに良いことに、タイプシグネチャのように聞こえるはずです。
標準的な数学的例には、モノイドidグループ⊃ベクトル空間⊃「代数」が含まれます。モノイドはオートマトンのようなものです:動詞のシーケンス(例えば、f.g.h.h.nothing.f.g.f
)。常に履歴を追加し、履歴を削除しないgit
ログは、グループではなくモノイドになります。逆数(負の数、分数、根、蓄積された履歴の削除、破損したミラーの粉砕解除など)を追加すると、グループが得られます。
グループには、一緒に加算または減算できるものが含まれています。たとえば、Duration
sを一緒に追加できます。 (ただしDate
sは使用できません。)デュレーションは外部の数値でスケーリングできるため、(グループだけでなく)ベクトル空間で存続します。 (scaling :: (Number,Duration) → Duration
の型シグネチャ。)
代数⊂ベクトル空間はさらに別のことを行うことができます:m :: (T,T) → T
があります。 Integers
を離れると、 "multiplication"(または "exponentiation" )がどうあるべきかがあまりわからなくなるため、これを「乗算」と呼びます。
(これが、人々が(カテゴリー理論)普遍的特性に目を向ける理由です:どの乗算がdoまたはのように:
)
共乗算は、乗算よりも、任意ではないように定義する方が簡単です。なぜなら、T → (T,T)
から進むには、同じ要素を繰り返すことができるからです。 (「対角マップ」–スペクトル理論の対角行列/演算子のように)
通常、Counitはトレース(対角エントリの合計)ですが、ここでも重要なのは、Counitdoes; trace
は、行列のちょうど良い答えです。
デュアルスペース を見る理由は、一般的に、そのスペースで考えるのが簡単だからです。たとえば、法線ベクトルについて考えるのは法線の平面よりも簡単な場合がありますが、ベクトルを使用して平面(超平面を含む)を制御できます(そして今ではレイトレーサーのようなおなじみの幾何学的ベクトルについて話します) 。
数学者は TQFT's のような楽しいものをモデリングしているかもしれませんが、プログラマーは
+ :: (Date,Duration) → Date
)、Paris
≠(+48.8567,+2.3508)
!これは形状であり、点ではありません。)、計算機科学者は、代数について話すとき、通常、デカルト積のように整然とした操作を念頭に置いています。これは人々が「代数はHaskellでの代数である」と言うときの意味だと思います。しかし、プログラマーがPlace
、Date/Time
、およびCustomer
のような複雑なデータ型をモデル化し、それらのモデルを実際の世界(または少なくとも終わり-現実世界に対するユーザーの見解)可能な限り-デュアルは、集合世界だけでなく有用であると考えています。