あなたは「欠けている一般的なパターン」を求めました。Petr Pudlákの答えもかなり良いですが、それを説明するために私自身の試みをさせてください. user3237465 が言うように、使用できるエンコーディングには Church と Scott の 2 つがあり、Church ではなく Scott を使用しています。というわけで、総評です。
エンコーディングの仕組み
継続渡しにより、型の任意の値を型x
の一意の関数で記述することができます
data Identity x = Id { runId :: x }
{- ~ - equivalent to - ~ -}
newtype IdentityFn x = IdFn { runIdFn :: forall z. (x -> z) -> z }
ここでの "forall" は非常に重要です。この型はz
未指定のパラメーターとして残されるということです。全単射とは、からからId . ($ id) . runIdFn
へIdentityFn
、逆Identity
にIdFn . flip ($) . runId
行くということです。等価性が生じるのは、基本的に type でできることは何もforall z. z
なく、操作が十分に普遍的ではないためです。newtype UnitFn = UnitFn { runUnitFn :: forall z. z -> z }
は 1 つの要素、つまり のみを持つと同等に述べることができます。これは、同様UnitFn id
にユニット タイプに対応することを意味します。data Unit = Unit
(x, y) -> z
に同型なカリー化観測x -> y -> z
は、データ構造を持たない純粋な関数の観点からデータ構造を表すことを可能にする継続通過氷山の一角Identity (x, y)
ですforall z. (x -> y -> z) -> z
。したがって、2 つのアイテムを「接着」することは、この型の値を作成することと同じであり、純粋な関数を「接着」として使用するだけです。
この同等性を確認するには、他の 2 つのプロパティを処理する必要があります。
1 つ目は、 の形式の合計型コンストラクターですEither x y -> z
。ほら、Either x y -> z
同形です
newtype EitherFn x y = EitherFn { runEitherFn :: forall z. (x -> z) -> (y -> z) -> z }
そこから、パターンの基本的なアイデアが得られます。
z
式の本体に表示されない新しい型変数を取得します。
- データ型のコンストラクターごとに、そのすべての型引数をパラメーターとして取り、. を返す関数型を作成します
z
。コンストラクターに対応するこれらの「ハンドラー」を呼び出します。したがって、 のハンドラ(x, y)
は(x, y) -> z
にカリー化されx -> y -> z
、 のハンドラLeft x | Right y
はx -> z
およびy -> z
です。パラメーターがない場合はz
、より面倒な ではなく、値を関数として使用できます() -> z
。
- これらのハンドラーをすべて式のパラメーターとして受け取ります
forall z. Handler1 -> Handler2 -> ... -> HandlerN -> z
。
- 同形の半分は、基本的に、コンストラクターを目的のハンドラーとして渡すことです。もう一方はコンストラクターでパターン マッチし、対応するハンドラーを適用します。
微妙に欠けているもの
繰り返しになりますが、これらのルールをさまざまなものに適用するのは楽しいものです。たとえば、上で述べたように、これを適用するdata Unit = Unit
と、任意の単位タイプが恒等関数forall z. z -> z
であることがわかります。これを適用するとdata Bool = False | True
、論理関数forall z. z -> z -> z
where false = const
whileが見つかりますtrue = const id
。しかし、実際に遊んでみると、まだ何かが足りないことに気付くでしょう。ヒント: 見てみると
data List x = Nil | Cons x (List x)
パターンは次のようになります。
data ListFn x = ListFn { runListFn :: forall z. z -> (x -> ??? -> z) -> z }
いくつかのために???
。上記のルールは、何がそこにあるのかを特定するものではありません。
2 つの適切なオプションがあります: の能力をnewtype
最大限に活用してそこに配置するか (「Scott」エンコーディング)、または与えられた関数を使用ListFn x
してプリエンプティブに削減することができます。z
私たちがすでに持っている機能(「教会」エンコーディング)。再帰はすでに事前に実行されているため、Church エンコーディングは有限データ構造に対してのみ完全に同等です。Scott エンコーディングは、無限リストなどを処理できます。チャーチ形式で相互再帰をエンコードする方法を理解するのも難しい場合がありますが、スコット形式は通常もう少し簡単です。
いずれにせよ、Church エンコーディングについて考えるのは少し難しいですが、希望的観測でアプローチすることができるので、もう少し魔法のようz
です。仕方。" そして、この希望的観測はまさに、この全単射の一方の側がまさにリストの側であるため、人々が理解に苦労する理由です。tail list
head list
foldr
foldr
他にも「コンストラクターの数が多かったり無限だったりしたらどうなるの?」というInt
ような問題があります。Integer
この特定の質問に対する答えは、関数を使用することです
data IntFn = IntFn { runIntFn :: forall z. (z -> z) -> z -> z }
これは何ですか?さて、賢い人 (教会) は、これが構成の繰り返しとして整数を表現する方法であることに気付きました。
zero f x = x
one f x = f x
two f x = f (f x)
{- ~ - increment an `n` to `n + 1` - ~ -}
succ n f = f . n f
実際、このアカウントm . n
は 2 つの製品です。()
しかし、私がこれについて言及するのは、 a を挿入して引数を反転させて、これが実際にはで与えられた値と で与えられた加算と で与えられた乗算を持つforall z. z -> (() -> z -> z) -> z
リスト型であることを見つけるのはそれほど難しくないからです。[()]
length
++
>>
効率を高めるためにdata PosNeg x = Neg x | Zero | Pos x
、Church エンコードし、Church エンコードを使用して (有限のままにします!)、それぞれが最後に最も重要なビットで明示されていない状態で暗黙的に終了する場所[Bool]
の Church エンコードを形成することができます。 +1 から無限に。PosNeg [Bool]
[Bool]
True
[Bool]
拡張例: BinLeaf / BL
もう 1 つの重要な例として、すべての情報を葉に格納するバイナリ ツリーについて考えることができますが、内部ノードには注釈も含まれますdata BinLeaf a x = Leaf x | Bin a (BinLeaf a x) (BinLeaf a x)
。Church エンコーディングのレシピに従って、次のことを行います。
newtype BL a x = BL { runBL :: forall z. (x -> z) -> (a -> z -> z -> z) -> z}
Bin "Hello" (Leaf 3) (Bin "What's up?" (Leaf 4) (Leaf 5)
インスタンスを小文字で構築する代わりに、次のようにします。
BL $ \leaf bin -> bin "Hello" (leaf 3) (bin "What's up?" (leaf 4) (leaf 5)
したがって、同型は非常に簡単ですbinleafFromBL f = runBL f Leaf Bin
。反対側はケース発送がありますが、悪くないです。
再帰データに対する再帰アルゴリズムはどうですか? ここが魔法のようです。foldr
そしてrunBL
、Church のエンコーディングは、ツリー自体に到達する前に、サブツリーで実行されていた関数を両方とも実行しました。たとえば、この関数をエミュレートしたいとします。
sumAnnotate :: (Num n) => BinLeaf a n -> BinLeaf (n, a) n
sumAnnotate (Leaf n) = Leaf n
sumAnnotate (Bin a x y) = Bin (getn x' + getn y', a) x' y'
where x' = sumAnnotate x
y' = sumAnnotate y
getn (Leaf n) = n
getn (Bin (n, _) _ _) = n
やらなければいけないことは何?
-- pseudo-constructors for BL a x.
makeLeaf :: x -> BL a x
makeLeaf x = BL $ \leaf _ -> leaf x
makeBin :: a -> BL a x -> BL a x -> BL a x
makeBin a l r = BL $ \leaf bin -> bin a (runBL l leaf bin) (runBL r leaf bin)
-- actual function
sumAnnotate' :: (Num n) => BL a n -> BL n n
sumAnnotate' f = runBL f makeLeaf (\a x y -> makeBin (getn x + getn y, a) x y) where
getn t = runBL t id (\n _ _ -> n)
関数を渡します\a x y -> ... :: (Num n) => a -> BL (n, a) n -> BL (n, a) n -> BL (n, a) n
。ここでは、2 つの「引数」が「出力」と同じタイプであることに注意してください。Church エンコーディングでは、すでに成功しているかのようにプログラミングする必要があります。これは「希望的観測」と呼ばれる規律です。
Free モナドの Church エンコーディング
Free モナドは正規形
data Free f x = Pure x | Roll f (Free f x)
そして、私たちの教会のエンコーディング手順は、これが次のようになると言います:
newtype Fr f x = Fr {runFr :: forall z. (x -> z) -> (f z -> z) -> z}
あなたの機能
matchFree p _ (Pure x) = p x
matchFree _ f (Free x) = f x
単純になる
matchFree' p f fr = runFr fr p f