余代数な有限機械のHaskell実装
概要
この記事は Bart Jacobs による "Introduction to Coalgebra" という大変面白い本の一部を Haskell で実装し確認したメモである。余代数や終余代数の概念をHaskell での実装を弾幕としてできるだけさりげなく導入する。またその適用例としてStream と有限オートマトンを構成する。結果として余代数が時間発展の構造(structure)を与えるとき、終余代数はその振る舞い(behaviour)を与えることを見る。また余帰納的(余再帰的)定義ということについても補足する。圏論の語彙は本質的な理解には差し替えないと思われる範囲で未定義のまま用いている。
Stream と余代数
Stream
有限オートマトンを扱う前に Stream
でウォーミングアップする。Stream
とはある型の値の無限個数の列である。いま Pair Functor
を次のように定義してみよう。
data Pair a b = Pair a b
instance Functor (Pair a) where
fmap f (Pair a b) = Pair a (f b)
すると Stream
は擬コードで
Stream a = Pair a (Pair a (Pair a ( ... )))
と定義することができる。型 a
を Pair a b
を Stream a
は a
の値を無限個数保持するデータ型であることが Pair
を用いて表現されている。
上の擬コードは次のように書き換えられる。
Stream a = Pair a (Stream a)
いまこの書き換えは
としたことに対応する。すなわち、Stream a
Pair a
なる Functor
そのような不動点は Data.Fix を用いて構成できる。
import Data.Fix
type Stream a = Fix (Pair a)
このままでは結果確認が不便なのでStream a
をリスト[a]
に変換するユーティリティを定義しておく。
toList :: Stream a -> [a]
toList = foldFix (elimPair [] (:))
where
elimPair x f (Pair a b) = f a b
foldFix
については補足で説明する。なおここでこの写像が全射でないことは留意すべき点である。実際、空リスト[]
へ写像される点は存在しない。
余代数
さて、余代数である。いま状態
を考えたい。ひとまとめにすると
である。一般に自己函手
なる射
は
であり、
は
c :: (s -> s) -> (s -> a) -> s -> Pair a s
c f g n = Pair (g n) (f n)
と実装できる。
終余代数
ここでは定義や定理による構成を割愛し結果論的に終余代数を導入する。
なる
を
なお、任意の
なる唯一の射が存在するとする(終対象 1 が存在するとする)。すると
なる列の極限 Stream a
がFix
は
newtype Fix f = Fix { unFix :: f (Fix f) }
で定義されており、Fix f = f (Fix f) = f (f (Fix f)) =...
である。
いま具体的に Stream a
であった。したがって a -> Stream a
ができればよい。いま
stream :: (a -> a) -> a -> Stream a
stream g = unfoldFix (c g id)
としよう。ここでunfoldFix
は型
unfoldFix :: Functor f => (a -> f a) -> a -> Fix f
を持ち、anamorphism とも呼ばれる。stream g
が求める関数a -> Stream a
となる。いま f
は Pair a
であり Stream a
は Fix (Pair a)
であった。
ここで例題として コラッツ問題 の関数
collatz :: Int -> Int
collatz n
| even n = div n 2
| otherwise = 3 * n + 1
を使ってみよう。実際に実行してみると
ghci> toList $ stream collatz 42
[42,21,64,32,16,8,4,2,1,4,2,1,4,2,1,...
となり、正しく計算できていることが分かる。これは Stream
をリストに変換しているため無限リストになる。もっと穏便な実行方法は例えば
ghci> takeWhile (1 /=) $ toList $ stream collatz 27
[27,82,41,124,62,31,94,47,142,71,214,107,322,161,484,242,121,364,182,91,274,137,412,206,103,310,155,466,233,700,350,175,526,263,790,395,1186,593,1780,890,445,1336,668,334,167,502,251,754,377,1132,566,283,850,425,1276,638,319,958,479,1438,719,2158,1079,3238,1619,4858,2429,7288,3644,1822,911,2734,1367,4102,2051,6154,3077,9232,4616,2308,1154,577,1732,866,433,1300,650,325,976,488,244,122,61,184,92,46,23,70,35,106,53,160,80,40,20,10,5,16,8,4,2]
である。ここから分かるように、終余代数は余代数の振る舞い(behaviour) を与えており、また余代数は終余代数の構造(structure) を与えている。ここでの余代数は終余代数を除いている。また、この振る舞いは「余代数から余帰納的に定義された」と表現できる。実際、Stream
型と stream
関数の定義は最大不動点演算子 Nu
を用いて
type Stream a = Nu (Pair a)
stream f = Nu (c f id)
と定義してもよい。この点については末尾に補足する。
有限オートマトンと余代数
多項式型
ここで型
すなわちその値は値なし
であるから、
data ListF a b = Nil | Cons a b
instance Functor (ListF a) where
fmap f (Cons a b) = Cons a (f b)
fmap f Nil = Nil
type List a = Fix (ListF a)
と定義すれば List a
が Nil | Cons a b
は ListF a
は函手 List a
が構成されている。
やはり便宜のためList a
とリスト[a]
の変換関数を定義しておく。
fromList :: [a] -> List a
fromList = unfoldFix fromListF
where
fromListF (x:xs) = Cons x xs
fromListF [] = Nil
toList :: List a -> [a]
toList = foldFix (elimListF [] (:))
where
elimListF x f (Cons a b) = f a b
elimListF x f Nil = x
Stream a
の場合とは異なりList a
と[a]
との間に全単射が存在しているため双方向の写像が定義できている。関数名の不適切さには目を瞑られたい。
有限オートマトン
有限オートマトンは状態
で表されるが、積
でこれを捉えられる。出力
なる余代数が有限オートマトンを決定することが分かる。
いま
と書こう。記号をこれに合わせると実装は
data F a b c = Pair (a -> c) b
getF (Pair f b) = f
getB (Pair f b) = b
instance Functor (F a b) where
fmap f (Pair g b) = Pair (f.g) b
c :: S -> F A B S
c s = Pair (delta s) (epsilon s)
となる。未定義の部分については有限オートマトンの具体形が必要である。ここでは"Introduction to Coalgebra"に掲載されていた
を実装してみよう。定義はひたすらしこしこ書くのみである。
data B = O | I deriving Show
type S = (B,B,B)
type A = B
epsilon :: S -> B
epsilon (I,I,I) = I
epsilon (I,O,O) = I
epsilon (I,O,I) = I
epsilon (I,I,O) = I
epsilon (O,O,O) = O
epsilon (O,O,I) = O
epsilon (O,I,I) = O
epsilon (O,I,O) = O
delta :: S -> A -> S
delta (O,O,O) O = (O,O,O)
delta (O,O,I) O = (O,I,O)
delta (O,I,I) O = (I,I,O)
delta (I,I,I) O = (I,I,O)
delta (O,O,O) I = (O,O,I)
delta (O,O,I) I = (O,I,I)
delta (O,I,I) I = (I,I,I)
delta (I,I,I) I = (I,I,I)
delta (I,O,O) O = (O,O,O)
delta (O,I,O) O = (I,O,O)
delta (I,O,I) O = (O,I,O)
delta (I,I,O) O = (I,I,I)
delta (I,O,O) I = (O,O,I)
delta (O,I,O) I = (I,O,I)
delta (I,O,I) I = (O,I,I)
delta (I,I,O) I = (I,O,I)
いま 0, 1 を O
, I
に代替している。
さて、この有限オートマトンの振る舞いを得るために終余代数を考えよう。函手
である。List a -> b
であり、
と書けば、有限オートマトンに任意長さの入力
Stream
の場合と同様に振る舞いは余帰納的定義
すなわち
b :: S -> Fix (F A B)
b = unfoldFix c
で与えられる。b
には初期状態を渡す。
ところで、いまStream a
のときほどは自明ではない。したがってその等価性
を関数で具体的に示す必要がある。List A -> B
のことであった。その関数はたとえば次のapply
で与えることができる。
apply :: Fix (F A B) -> List A -> B
apply f as =
case as of
Fix Nil -> getB $ unFix f
Fix (Cons a as) -> apply ((getF $ unFix f) a) as
いくつか具体例を計算してみよう。
ghci> (apply $ b (O,O,O)) $ fromList [I,O,O]
I
ghci> (apply $ b (O,O,O)) $ fromList [I,I,O,I,I]
O
ghci> (apply $ b (I,I,O)) $ fromList [I,I,O]
I
上の状態遷移図を辿ると正しく計算できていることが確認できる。
(補足)帰納/余帰納について
ここでの内容は Benjamin Pierceによる "Types and Programming Languages" を参考にしている。
対象と射
写像
-
のときF(X) \subseteq X はX -closed であるという。F -
のときX \subseteq F(X) はX -consistent であるという。F -
のときX = F(X) はX の不動点であるという。F
このとき次の性質が成り立つ(Knaster-Tarski の定理)。
ここで
- 帰納法:
がX -closed ならばF \mu F \subseteq X - 余帰納法:
がX -consistent ならばF X \subseteq \nu F
以上の内容を圏論に翻案すると次のように言葉を定義できよう。
-
-代数F に対して始代数F(X) \rightarrow X が存在するとき、帰納(再帰)射F(\mu F)=\mu F が存在する。\mu F \rightarrow X -
-余代数F に対して終余代数X\rightarrow F(X) が存在するとき、余帰納(余再帰)射\nu F = F(\nu F) が存在する。X \rightarrow \nu F
上ではまさしく余帰納射 Stream
や有限オートマトンを扱ったのであった。
Stream a
の擬コードによる定義は
Stream a = Pair a (Stream a)
であった。したがって既存のStream
に a
型の値を追加し、新たなStream
とすることができるはずである。すなわち次なる関数が存在するはずである。
Pair a (Stream a) -> Stream a
これはまさしく代数 Fix
をそのまま該当する関数として用いることができる。実際、次のような挙動を示す。
ghci> toList $ stream (+1) 1
[1,2,3,4,5,6,7,8,9,10,...
ghci> toList $ Fix $ Pair 0 (stream (+1) 1)
[0,1,2,3,4,5,6,7,8,9,..
今度はList
を使ってみる(Stream
の節と同名異定義の関数を用いているので注意せよ)。次のような関数を定義しよう。
f :: ListF Int Int -> Int
f (Cons a b) = a * b
f Nil = 1
prod :: List Int -> Int
prod = foldFix f
f
は数値の積 List a
型は不動点Fix (ListF a)
として定義されていた。終余代数と同様に始代数
を考える。それは
foldFix :: Functor f => (f a -> a) -> Fix f -> a
で与えられ、catamorphismとも呼ばれる。prod
は総積関数になる。実際、
ghci> prod $ fromList [1,2,3,4,5]
120
である。ここで
g :: Int -> List F Int Int
g 0 = Nil
g n = Cons n (n-1)
とすればg
は余代数を与える。
ghci> toList $ unfoldFix g 5
[5,4,3,2,1]
となる。f
と g
を組み合わせれば階乗関数が作れる。
ghci> foldFix f $ unfoldFix g 5
120
したがって俗に言われる「階乗の再帰的定義」は帰納(再帰)と余帰納(余再帰)の双方がこれを構成しており語義を混乱させている。
個人的思想的なこと
時間発展方程式に初期値を与えて軌道を求める、古典力学でおなじみの行為は、余代数の終余代数への余帰納射を与えることであった。私たちに観察できるのはその振る舞いのみであるが、しばしば(それが決定的であれ非決定的であれ、確率的であれ)そこに構造を見出すことで思考を巡らせている。参考文献"Introduction to Coalgebra"では有限オートマトンにおいて振る舞いの圏と構造の圏が随伴であることを示している。
私たちの時間観は巨視的な履歴現象に基づいており、「順を追って」考える習慣のある人間にとって、構造と振る舞いとしての余代数と終余代数はかなり卑近である。一方で自然界の振る舞いは元をただすと量子現象であり、自発的確率的な相互作用が基本にある。そこには私たちの時間観を可能にするものがあるが、私たちの時間観をどこまで適用できるかは自明ではない。余代数の概念をうまく「量子化」できればそのような問題にもアプローチできるかもしれない。
参考文献まとめ
- Bart Jacobs ,"Introduction to Coalgebra"
- Benjamin Pierce, "Types and Programming Languages"
Discussion