それらは同型ですか?
はい、Haskellでは同型です。その他の注意点については、Ed Kmettの再帰スキームパッケージのFix、Mu、Nuの違いを参照してください。
もしそうなら、どうやってそれを証明しますか?
まず、変換を実行する関数を定義します。
muToFix :: Mu f -> Fix f
muToFix (Mu s) = s Fix
fixToMu :: Functor f => Fix f -> Mu f
fixToMu t = Mu (\alg -> cata alg t)
これらの関数が同型であることを示すには、次のことを示す必要があります。
muToFix . fixToMu = id
fixToMu . muToFix = id
Fix
バック
同型の方向の1つは、他の方向よりもいくぶん簡単に外れます。
muToFix (fixToMu t) = t
muToFix (fixToMu t) -- LHS
muToFix (Mu (\f -> cata f t))
(\f -> cata f t) Fix
cata Fix t -- See below.
t -- LHS = RHS
上記の最後の節cata Fix t = t
は、次の定義で確認できますcata
。
cata :: Functor f => (f a -> a) -> Fix f -> a
cata alg = alg . fmap (cata alg) . unfix
cata Fix t
次に、ですFix (fmap (cata Fix) (unfix t))
。帰納法を使用してt
、少なくとも有限の場合、それがでなければならないことを示すことができますt
(無限の構造ではより微妙になります。この回答の最後にある補遺を参照してください)。考慮すべき2つの可能性があります。
unfix t :: f (Fix f)
空であり、掘り下げる再帰位置はありません。その場合、それはfmap absurd z
some と等しい必要があるためz :: f Void
、次のようになります。
cata Fix t
Fix (fmap (cata Fix) (unfix t))
Fix (fmap (cata Fix) (fmap absurd z))
Fix (fmap (cata Fix . absurd) z)
-- fmap doesn't do anything on an empty structure.
Fix (fmap absurd z)
Fix (unfix t)
t
unfix t
空ではありません。その場合、少なくとも、再帰的な位置にfmap (cata Fix)
適用する以上のことはできないことがわかっていますcata Fix
。ここでの帰納法の仮説は、そうすることでそれらの位置が変更されないままになるというものです。次に、
cata Fix t
Fix (fmap (cata Fix) (unfix t))
Fix (unfix t) -- Induction hypothesis.
t
(結局のところ、これcata Fix = id
はFix :: f (Fix f) -> Fix x
初期のF代数であることの帰結です。この証明のコンテキストでその事実に直接頼ることは、おそらくあまりにも多くのショートカットになるでしょう。)
Mu
バック
与えられたmuToFix . fixToMu = id
、fixToMu . muToFix = id
いずれかを証明することで十分であることを証明するには:
それmuToFix
は単射的です、または
それfixToMu
は全容です。
2番目のオプションを使用して、関連する定義を確認します。
newtype Mu f = Mu (forall a. (f a -> a) -> a)
fixToMu :: Functor f => Fix f -> Mu f
fixToMu t = Mu (\alg -> cata alg t)
fixToMu
つまり、全射的であることは、特定のが与えられた場合Functor
f
、型のすべての関数を特定のに対してforall a. (f a -> a) -> a
として定義できることを意味します。タスクは、関数をカタログ化し、それらすべてをその形式で表現できるかどうかを確認することになります。\alg -> cata alg t
t :: Fix f
forall a. (f a -> a) -> a
forall a. (f a -> a) -> a
頼りにせずに関数を定義するにはどうすればよいfixToMu
ですか?何があっても、結果f a -> a
を取得するには、引数として指定された代数を使用する必要がありますa
。直接ルートは、それをあるf a
値に適用することになります。主な注意点は、a
ポリモーフィックであるf a
ため、任意の選択に対して上記の値を想起させる必要があるということですa
。それが実行可能な戦略である限り、それは実行可能な戦略ですf
価値が存在するです。その場合、次のことができます。
fromEmpty :: Functor f => f Void -> forall a. (f a -> a) -> a
fromEmpty z = \alg -> alg (fmap absurd z)
表記をわかりやすくするために、forall a. (f a -> a) -> a
関数の定義に使用できるもののタイプを定義しましょう。
data Moo f = Empty (f Void)
fromMoo :: Functor f => Moo f -> forall a. (f a -> a) -> a
fromMoo (Empty z) = \alg -> alg (fmap absurd z)
直接ルート以外にも、可能性は1つだけあります。それを考えるとf
ありFunctor
、我々は何とか持っている場合、f (Moo f)
外側の下にある最初のアプリケーション、値を私たちは二度代数を適用することができますf
層を介してfmap
とfromMoo
:
fromLayered :: Functor f => f (Moo f) -> forall a. (f a -> a) -> a
fromLayered u = \alg -> alg (fmap (\moo -> fromMoo moo alg) u)
値forall a. (f a -> a) -> a
からも作成できることを考えるとf (Moo f)
、それらを次の場合として追加することは理にかなっていますMoo
。
data Moo f = Empty (f Void) | Layered (f (Moo f))
したがって、以下fromLayered
に組み込むことができますfromMoo
。
fromMoo :: Functor f => Moo f -> forall a. (f a -> a) -> a
fromMoo = \case
Empty z -> \alg -> alg (fmap absurd z)
Layered u -> \alg -> alg (fmap (\moo -> fromMoo moo alg) u)
こうすることalg
で、1つのf
レイヤーの下に適用することから、再帰的に適用することにこっそりと移動したことに注意してください。alg
、任意の数のf
レイヤーの下。
次に、f Void
値をLayered
コンストラクターに注入できることに注意してください。
emptyLayered :: Functor f => f Void -> Moo f
emptyLayered z = Layered (fmap absurd z)
つまり、実際にはEmpty
コンストラクタは必要ありません。
newtype Moo f = Moo (f (Moo f))
unMoo :: Moo f -> f (Moo f)
unMoo (Moo u) = u
のEmpty
場合はfromMoo
どうですか?2つのケースの唯一の違いは、このEmpty
ケースでは、のabsurd
代わりにがあるということです\moo -> fromMoo moo alg
。すべてのVoid -> a
関数はabsurd
なので、別のEmpty
ケースは必要ありません。
fromMoo :: Functor f => Moo f -> forall a. (f a -> a) -> a
fromMoo (Moo u) = \alg -> alg (fmap (\moo -> fromMoo moo alg) u)
考えられる表面的な微調整は、fromMoo
引数を反転することです。そのため、引数をfmap
ラムダとして書き込む必要はありません。
foldMoo :: Functor f => (f a -> a) -> Moo f -> a
foldMoo alg (Moo u) = alg (fmap (foldMoo alg) u)
または、よりポイントフリー:
foldMoo :: Functor f => (f a -> a) -> Moo f -> a
foldMoo alg = alg . fmap (foldMoo alg) . unMoo
この時点で、定義をもう一度見てみると、名前の変更が適切であることを示しています。
newtype Fix f = Fix (f (Fix f))
unfix :: Fix f -> f (Fix f)
unfix (Fix u) = u
cata :: Functor f => (f a -> a) -> Fix f -> a
cata alg = alg . fmap (cata alg) . unfix
fromFix :: Functor f => Fix f -> forall a. (f a -> a) -> a
fromFix t = \alg -> cata alg t
そしてそれがあります:すべてのforall a. (f a -> a) -> a
関数は\alg -> cata alg t
いくつかのためのフォームを持っていt :: Fix f
ます。したがって、fixToMu
全射であり、望ましい同型性があります。
補遺
コメントでは、cata Fix t = t
導出における帰納法の適用性について緊密な質問が出されました。少なくとも、ファンクタの法則とパラメトリック性は、fmap (cata Fix)
余分な作業を作成しないことを保証します(たとえば、構造を拡大したり、掘り下げる追加の再帰的な位置を導入したりしない)。誘導の誘導ステップで問題になります。そのため、t
有限構造の場合、f (Fix t)
最終的には空の基本ケースに到達し、すべてが明確になります。私たちが許可した場合t
、無限であることを、しかし、我々は、際限なく下降維持することができますfmap
後にfmap
した後fmap
、これまで基本ケースに到達することなく、。
ただし、無限構造の状況は、最初に思われるほどひどいものではありません。そもそも無限構造を実行可能にするものである怠惰により、無限構造を遅延して消費することができます。
GHCi> :info ListF
data ListF a b = Nil | Cons a b
-- etc.
GHCi> ones = Fix (Cons 1 ones)
GHCi> (\(Fix (Cons a _)) -> a) (cata Fix ones)
1
GHCi> (\(Fix (Cons _ (Fix (Cons a _)))) -> a) (cata Fix ones)
1
再帰的な位置の連続は無限に広がりますが、任意の時点で停止して、周囲のListF
関数コンテキストから有用な結果を得ることができます。このようなコンテキストは繰り返し発生し、の影響を受けないfmap
ため、使用する可能性のある構造の有限セグメントはの影響を受けませんcata Fix
。
この怠惰猶予は、この議論の他の箇所で述べた方法を反映し、怠惰は、固定点間の区別を崩壊Mu
、Fix
およびNu
。怠惰なしでFix
は、生産的な共起をエンコードするのに十分ではないためNu
、最大の固定小数点であるに切り替える必要があります。ここに違いの小さなデモがあります:
GHCi> :set -XBangPatterns
GHCi> -- Like ListF, but strict in the recursive position.
GHCi> data SListF a b = SNil | SCons a !b deriving Functor
GHCi> ones = Nu (\() -> SCons 1 ()) ()
GHCi> (\(Nu c a) -> (\(SCons a _) -> a) (c a)) ones
1
GHCi> ones' = Fix (SCons 1 ones')
GHCi> (\(Fix (SCons a _)) -> a) ones'
^CInterrupted.