私はそれを理解することになりました。私が行った講演のビデオとスライドをご覧ください。
元の質問:
一般的な再帰スキーム(つまり、Fix
を使用する再帰スキーム)を理解するための努力の中で、さまざまなスキームのリストのみのバージョンを記述することが有用であることがわかりました。これにより、実際のスキームをより簡単に理解できます(Fix
の追加オーバーヘッドなし)。
ただし、zygo
およびfutu
のリストのみのバージョンを定義する方法はまだわかりません。
これまでの私の専門的な定義は次のとおりです。
cataL :: (a -> b -> b) -> b -> [a] -> b
cataL f b (a : as) = f a (cataL f b as)
cataL _ b [] = b
paraL :: (a -> [a] -> b -> b) -> b -> [a] -> b
paraL f b (a : as) = f a as (paraL f b as)
paraL _ b [] = b
-- TODO: histo
-- DONE: zygo (see below)
anaL :: (b -> (a, b)) -> b -> [a]
anaL f b = let (a, b') = f b in a : anaL f b'
anaL' :: (b -> Maybe (a, b)) -> b -> [a]
anaL' f b = case f b of
Just (a, b') -> a : anaL' f b'
Nothing -> []
apoL :: ([b] -> Maybe (a, Either [b] [a])) -> [b] -> [a]
apoL f b = case f b of
Nothing -> []
Just (x, Left c) -> x : apoL f c
Just (x, Right e) -> x : e
-- DONE: futu (see below)
hyloL :: (a -> c -> c) -> c -> (b -> Maybe (a, b)) -> b -> c
hyloL f z g = cataL f z . anaL' g
hyloL' :: (a -> c -> c) -> c -> (c -> Maybe (a, c)) -> c
hyloL' f z g = case g z of
Nothing -> z
Just (x,z') -> f x (hyloL' f z' g)
リストのhisto
、zygo
、およびfutu
はどのように定義しますか?
Zygomorphismは、2つのsemi-mutually再帰関数から構築されたフォールドに付ける、高確率の数学的な名前です。例を挙げましょう。
pm :: [Int] -> Int
と+
が交互に数字のリストを介して散らばるpm [v,w,x,y,z] = v - (w + (x - (y + z)))
などの関数-
(plus-minusの場合)を想像してください。プリミティブな再帰を使用して書き出すことができます。
lengthEven :: [a] -> Bool
lengthEven = even . length
pm0 [] = 0
pm0 (x:xs) = if lengthEven xs
then x - pm0 xs
else x + pm0 xs
明らかにpm0
は compositional ではありません-各位置でリスト全体の長さを調べて、加算するか減算するかを決定する必要があります。 Paramorphismは、フォールディング関数がフォールドの各反復でサブツリー全体をトラバースする必要がある場合、この種のプリミティブな再帰をモデル化します。そのため、少なくとも、確立されたパターンに適合するようにコードを書き換えることができます。
paraL :: (a -> [a] -> b -> b) -> b -> [a] -> b
paraL f z [] = z
paraL f z (x:xs) = f x xs (paraL f z xs)
pm1 = paraL (\x xs acc -> if lengthEven xs then x - acc else x + acc) 0
しかし、これは非効率的です。 lengthEven
は、パラモーフィズムの各反復でリスト全体を走査し、O(n2)アルゴリズム。
lengthEven
とpara
の両方がfoldr
...でcatamorphismとして表現できることに注意することで前進できます。
cataL = foldr
lengthEven' = cataL (\_ p -> not p) True
paraL' f z = snd . cataL (\x (xs, acc) -> (x:xs, f x xs acc)) ([], z)
...これは、2つの操作をリスト上の1つのパスに融合できることを示唆しています。
pm2 = snd . cataL (\x (isEven, total) -> (not isEven, if isEven
then x - total
else x + total)) (True, 0)
別のフォールドの結果に依存するフォールドがあり、それらを1つのリストのトラバーサルに融合することができました。接合型は、まさにこのパターンをキャプチャします。
zygoL :: (a -> b -> b) -> -- a folding function
(a -> b -> c -> c) -> -- a folding function which depends on the result of the other fold
b -> c -> -- zeroes for the two folds
[a] -> c
zygoL f g z e = snd . cataL (\x (p, q) -> (f x p, g x p q)) (z, e)
フォールドの各反復で、f
はカタモフィズムのように最後の反復からの回答を見ますが、g
は両方の関数の回答を見ます。 g
はf
と絡まります。
最初の折りたたみ関数を使用してリストの長さが偶数か奇数かをカウントし、2番目のリストを使用して合計を計算することにより、pm
をザイモモルフィズムとして記述します。
pm3 = zygoL (\_ p -> not p) (\x isEven total -> if isEven
then x - total
else x + total) True 0
これは、古典的な関数型プログラミングスタイルです。リストを消費するという重いリフティングを行う高階関数があります。ロジックをプラグインして結果を集計するだけでした。構成は明らかに終了します(foldr
の終了を証明するだけで済みます)。また、ブートするのに元の手書きバージョンよりも効率的です。
Aside:@AlexRは、zygomorphismにはmutumorphismと呼ばれる姉がいるとコメントで指摘しています。栄光。
mutu
はzygo
を一般化します両方折り畳み関数は、前の反復からの他方の結果を検査できます。mutuL :: (a -> b -> c -> b) -> (a -> b -> c -> c) -> b -> c -> [a] -> c mutuL f g z e = snd . cataL (\x (p, q) -> (f x p q, g x p q)) (z, e)
余分な引数を無視するだけで、
zygo
からmutu
を回復できます。zygoL f = mutuL (\x p q -> f x p)
もちろん、これらの折り畳みパターンはすべて、リストから任意のファンクターの不動点に一般化されます。
newtype Fix f = Fix { unFix :: f (Fix f) }
cata :: Functor f => (f a -> a) -> Fix f -> a
cata f = f . fmap (cata f) . unFix
para :: Functor f => (f (Fix f, a) -> a) -> Fix f -> a
para f = snd . cata (\x -> (Fix $ fmap fst x, f x))
zygo :: Functor f => (f b -> b) -> (f (b, a) -> a) -> Fix f -> a
zygo f g = snd . cata (\x -> (f $ fmap fst x, g x))
mutu :: Functor f => (f (b, a) -> b) -> (f (b, a) -> a) -> Fix f -> a
mutu f g = snd . cata (\x -> (f x, g x))
zygo
の定義とzygoL
の定義を比較します。また、zygo Fix = para
、および後の3つのフォールドはcata
の観点から実装できることに注意してください。フォルドロジーでは、すべてが他のすべてに関連しています。
汎用バージョンからリストバージョンを回復できます。
data ListF a r = Nil_ | Cons_ a r deriving Functor
type List a = Fix (ListF a)
zygoL' :: (a -> b -> b) -> (a -> b -> c -> c) -> b -> c -> List a -> c
zygoL' f g z e = zygo k l
where k Nil_ = z
k (Cons_ x y) = f x y
l Nil_ = e
l (Cons_ x (y, z)) = g x y z
pm4 = zygoL' (\_ p -> not p) (\x isEven total -> if isEven
then x - total
else x + total) True 0
Histomorphismモデルdynamic programming、以前のサブ計算の結果を集計する手法。 ( 値の帰納法 と呼ばれることもあります。)組織型では、フォールディング関数はフォールドの以前の反復の結果のテーブルにアクセスできます。これを、畳み込み関数が最後の反復の結果のみを見ることができるカタモフィズムと比較してください。組織型には後知恵があります-すべての歴史を見ることができます。
これがアイデアです。入力リストを使用すると、フォールディング代数はb
sのシーケンスを出力します。 histo
は、各b
が出現するたびに書き留めて、結果のテーブルに添付します。履歴内のアイテムの数は、処理したリストレイヤーの数に等しくなります。リスト全体を破棄するまでに、操作の履歴の長さはリストの長さと等しくなります。
これは、リスト(ory)の繰り返しの履歴のようです:
data History a b = Ancient b | Age a b (History a b)
History
は、事物と結果のペアのリストであり、最後に[]
- thingに対応する追加の結果があります。入力リストの各レイヤーを対応する結果とペアにします。
cataL = foldr
history :: (a -> History a b -> b) -> b -> [a] -> History a b
history f z = cataL (\x h -> Age x (f x h) h) (Ancient z)
リスト全体を右から左に折り畳むと、最終結果はスタックの一番上になります。
headH :: History a b -> b
headH (Ancient x) = x
headH (Age _ x _) = x
histoL :: (a -> History a b -> b) -> b -> [a] -> b
histoL f z = headH . history f z
(History a
は comonad ですが、headH
(néeextract
)はhistoL
を定義するために必要なすべてです。)
History
は、入力リストの各レイヤーに対応する結果をラベル付けします。 cofree comonadは、任意の構造の各層にラベルを付けるパターンをキャプチャします。
data Cofree f a = Cofree { headC :: a, tailC :: f (Cofree f a) }
(History
をListF
に接続して単純化することで、Cofree
を思いつきました。)
これをfreeモナドと比較し、
data Free f a = Free (f (Free f a))
| Return a
Free
は連産品タイプです。 Cofree
は製品タイプです。 Free
はf
sのラザニアを重ね、ラザニアの下部に値a
を付けます。 Cofree
は、各層でa
の値でラザニアを重ねます。無料のモナドは、一般化された外部ラベル付きツリーです。 cofree comonadsは、一般化された内部ラベル付きツリーです。
Cofree
を手にすると、リストから任意のファンクターの不動点に一般化できます。
newtype Fix f = Fix { unFix :: f (Fix f) }
cata :: Functor f => (f b -> b) -> Fix f -> b
cata f = f . fmap (cata f) . unFix
histo :: Functor f => (f (Cofree f b) -> b) -> Fix f -> b
histo f = headC . cata (\x -> Cofree (f x) x)
そしてもう一度リストのバージョンを回復します。
data ListF a r = Nil_ | Cons_ a r deriving Functor
type List a = Fix (ListF a)
type History' a b = Cofree (ListF a) b
histoL' :: (a -> History' a b -> b) -> b -> List a -> b
histoL' f z = histo g
where g Nil_ = z
g (Cons_ x h) = f x h
Aside:
histo
はfutu
の双対です。それらのタイプを見てください。histo :: Functor f => (f (Cofree f a) -> a) -> (Fix f -> a) futu :: Functor f => (a -> f (Free f a)) -> (a -> Fix f)
futu
は、矢印が反転し、histo
がFree
に置き換えられたCofree
です。組織型は過去を見る;未来型は未来を予測します。cata f . ana g
を準同型に融合できるように、histo f . futu g
を chronomorphism 。
数学的な部分を省略しても、 HinzeとWuによるこのペーパー には、組織型とその使用法に関する優れた、例に基づいたチュートリアルがあります。
futu
について他の誰もまだ答えていないので、つまずくようにします。 _ListF a b = Base [a] = ConsF a b | NilF
_を使用します
_recursion-schemes
_ :futu :: Unfoldable t => (a -> Base t (Free (Base t) a)) -> a -> t
で型を取得します。
Unfoldable
制約を無視して、t
を_[b]
_に置き換えます。
_(a -> Base [b] (Free (Base [b]) a)) -> a -> [b]
(a -> ListF b (Free (ListF b) a)) -> a -> [b]
_
Free (ListF b) a)
はリストであり、末尾にa
型の穴がある場合があります。これは、_([b], Maybe a)
_と同型であることを意味します。だから今、私たちは持っています:
_(a -> ListF b ([b], Maybe a)) -> a -> [b]
_
最後のListF
を削除し、_ListF a b
_がMaybe (a, b)
と同型であることを確認します。
_(a -> Maybe (b, ([b], Maybe a))) -> a -> [b]
_
さて、型テトリスをプレイすることが唯一の賢明な実装につながると確信しています。
_futuL f x = case f x of
Nothing -> []
Just (y, (ys, mz)) -> y : (ys ++ fz)
where fz = case mz of
Nothing -> []
Just z -> futuL f z
_
結果の関数を要約すると、futuL
はシード値と少なくとも1つの結果を生成する可能性のある関数を受け取り、結果が生成された場合は新しいシード値を取得します。
最初はこれが同等だと思った
_notFutuL :: (a -> ([b], Maybe a)) -> a -> [b]
notFutuL f x = case f x of
(ys, mx) -> ys ++ case mx of
Nothing -> []
Just x' -> notFutuL f x'
_
そして実際には、多かれ少なかれ、しかし、1つの重要な違いは、実際のfutu
が生産性を保証することです(つまり、f
が常に戻る場合、次のリスト要素)。