単一リンクリストを考えてみましょう。それは次のようになります
data List x = Node x (List x) | End
次のようなフォールディング関数を定義するのは自然です
reduce :: (x -> y -> y) -> y -> List x -> y
ある意味で、reduce f x0
は、すべてのNode
をf
に、すべてのEnd
をx0
に置き換えます。これは、プレリュードがfoldと呼ぶものです。
ここで、単純なバイナリツリーを考えます。
data Tree x = Leaf x | Branch (Tree x) (Tree x)
同様に、次のような関数を定義することは自然です
reduce :: (y -> y -> y) -> (x -> y) -> Tree x -> y
thisの削減にはまったく異なる性質があることに注意してください。リストベースのものは本質的にシーケンシャルですが、この新しいツリーベースのものは、分割統治の感覚をより多く持っています。そこにいくつかのpar
コンビネータを投入することを想像することもできます。 (リストバージョンのどこにそのようなものを入れますか?)
私の質問:この関数はまだ「フォールド」として分類されていますか、それとも別のものですか? (もしそうなら、それは何ですか?)
基本的に、誰かがフォールディングについて話すときはいつでも、彼らは常にフォールディングについて話しますlists、これは本質的にシーケンシャルです。 「シーケンシャル」がフォールドとは何かの定義の一部であるのか、またはこれがフォールディングの最も一般的に使用される例の単なる偶然の特性であるのかと思います。
Tikhonは技術的なものを落としました。私は彼の言ったことから簡略化しようと思う。
「折りたたみ」という用語は、残念ながら、長年にわたってあいまいになり、次の2つのうちのいずれかを意味します。
Foldable
クラスでの「折りたたみ」の意味です。これらの概念の両方を総称的に定義して、1つのパラメーター化された関数がさまざまな型に対してそれを実行できるようにすることができます。 2番目のケースでは、Tikhonがその方法を示します。
しかし、ほとんどの場合、Fix
と代数などを使用するのはやりすぎです。代数的データ型のフォールドを書く簡単な方法を考えてみましょう。例として、Maybe
、ペア、リスト、ツリーを使用します。
_data Maybe a = Nothing | Just a
data Pair a b = Pair a b
data List a = Nil | Cons a (List a)
data Tree x = Leaf x | Branch (Tree x) (Tree x)
data BTree a = Empty | Node a (BTree a) (BTree a)
_
Pair
は再帰的ではないことに注意してください。これから説明する手順では、「fold」タイプが再帰的であるとは想定していません。人々は通常、このケースを「フォールド」とは呼びませんが、実際には、同じ概念の非再帰的なケースです。
最初のステップ:特定のタイプのフォールドはフォールドタイプを消費し、その結果としていくつかのパラメータータイプを生成します。後者のr
(「結果」)を呼び出すのが好きです。そう:
_foldMaybe :: ... -> Maybe a -> r
foldPair :: ... -> Pair a b -> r
foldList :: ... -> List a -> r
foldTree :: ... -> Tree a -> r
foldBTree :: ... -> BTree a -> r
_
2番目のステップ:最後の引数(構造体の引数)に加えて、foldは型がコンストラクターを持っているのと同じ数の引数を取ります。 Pair
には1つのコンストラクターがあり、他の例には2つあります。
_foldMaybe :: nothing -> just -> Maybe a -> r
foldPair :: pair -> Pair a b -> r
foldList :: nil -> cons -> List a -> r
foldTree :: leaf -> branch -> Tree a -> r
foldBTree :: empty -> node -> BTree a -> r
_
3番目のステップ:これらの各引数には、対応するコンストラクターと同じアリティがあります。コンストラクタを関数として扱い、それらの型を書き出します(型変数が、作成しているシグネチャの変数と一致することを確認してください)。
_Nothing :: Maybe a
Just :: a -> Maybe a
Pair :: a -> b -> Pair a b
Nil :: List a
Cons :: a -> List a -> List a
Leaf :: a -> Tree a
Branch :: Tree a -> Tree a -> Tree a
Empty :: BTree a
Node :: a -> BTree a -> BTree a -> BTree a
_
ステップ4:各コンストラクターのシグニチャーで、構築するデータ型のすべての出現箇所を型変数r
(フォールドシグニチャーで使用している)に置き換えます。
_nothing := r
just := a -> r
pair := a -> b -> r
nil := r
cons := a -> r -> r
leaf := a -> r
branch := r -> r -> r
empty := r
node := a -> r -> r -> r
_
ご覧のとおり、結果のシグネチャを2番目のステップからのダミータイプ変数に「割り当て」ました。今ステップ5:それらを以前のスケッチ折りの署名に記入します。
_foldMaybe :: r -> (a -> r) -> Maybe a -> r
foldPair :: (a -> b -> r) -> Pair a b -> r
foldList :: r -> (a -> r -> r) -> List a -> r
foldTree :: (a -> r) -> (r -> r -> r) -> Tree a -> r
foldBTree :: r -> (a -> r -> r -> r) -> BTree a -> r
_
現在、これらはこれらのタイプの折りたたみのシグネチャです。 data
宣言とコンストラクター型から折りたたみ型を読み取ることでこれを機械的に行ったので、引数の順序はおかしいですが、関数プログラミングでは、何らかの理由により、基本ケースをdata
定義の最初に置き、再帰的なケースハンドラーを最初にfold
定義に置くのが一般的です。問題ない!それらを改造して、より一般的なものにしましょう:
_foldMaybe :: (a -> r) -> r -> Maybe a -> r
foldPair :: (a -> b -> r) -> Pair a b -> r
foldList :: (a -> r -> r) -> r -> List a -> r
foldTree :: (r -> r -> r) -> (a -> r) -> Tree a -> r
foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
_
定義は機械的に入力することもできます。 foldBTree
を選択して、段階的に実装しましょう。特定のタイプのフォールドは、この条件を満たす、私たちが考え出したタイプの1つの関数です。タイプのコンストラクターを使用したフォールディングは、そのタイプに対する恒等関数です(最初の値と同じ結果が得られます)。
次のように始めます。
_foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree = ???
_
3つの引数を取ることがわかっているので、変数を追加してそれらを反映できます。長い説明的な名前を使用します。
_foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty tree = ???
_
data
宣言を見ると、BTree
には2つの可能なコンストラクターがあることがわかります。定義をそれぞれのケースに分割し、それらの要素の変数を入力できます。
_foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = ???
foldBTree branch empty (Branch a l r) = ???
-- Let's use comments to keep track of the types:
-- a :: a
-- l, r :: BTree a
_
さて、undefined
のようなものが不足していますが、最初の方程式を埋める唯一の方法はempty
を使用することです:
_foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = empty
foldBTree branch empty (Branch a l r) = ???
-- a :: a
-- l, r :: BTree a
_
2番目の方程式をどのように入力しますか?繰り返しになりますが、undefined
が不足している場合は、次のようになります。
_branch :: a -> r -> r -> r
a :: a
l, r :: BTree a
_
_subfold :: BTree a -> r
_があれば、branch a (subfold l) (subfold r) :: r
を実行できます。もちろん、「サブフォールド」は簡単に作成できます。
_foldBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
foldBTree branch empty Empty = empty
foldBTree branch empty (Branch a l r) = branch a (subfold l) (subfold r)
where subfold = foldBTree branch empty
_
_foldBTree Branch Empty anyTree == anyTree
_であるため、これはBTree
のフォールドです。 foldBTree
がこのタイプの唯一の関数ではないことに注意してください。これもあります:
_mangleBTree :: (a -> r -> r -> r) -> r -> BTree a -> r
mangleBTree branch empty Empty = empty
mangleBTree branch empty (Branch a l r) = branch a (submangle r) (submangle l)
where submangle = mangleBTree branch empty
_
ただし、一般的に、mangleBTree
には必要なプロパティがありません。たとえば、foo = Branch 1 (Branch 2 Empty Empty) Empty
がある場合は、_mangleBTree Branch Empty foo /= foo
_になります。したがって、mangleBTree
は正しいタイプですが、フォールドではありません。
ここで、詳細から一歩戻り、mangleTree
の例を使用して、最後のポイントに集中しましょう。フォールド(構造的な意味では、私の答えの先頭にある#2)は、代数型の最も単純で重要な関数にすぎません。そのため、型のコンストラクターに引数としてパスを渡すと、それはそのタイプの恒等関数になります。 (自明ではないが、_foo f z xs = xs
_のようなものは許可されないことを意味する。)
これは非常に重要です。私が考えたい2つの方法は次のとおりです。
tail :: [a] -> [a]
_をfoldr
、_(:)
_、および_[]
_を付けて書いてみてください。2番目のポイントはさらに進んで、コンストラクターも必要ありません。フォールドのみを使用して、data
宣言またはコンストラクターを使用せずに任意の代数型を実装できます。
_{-# LANGUAGE RankNTypes #-}
-- | A Church-encoded list is a function that takes the two 'foldr' arguments
-- and produces a result from them.
newtype ChurchList a =
ChurchList { runList :: forall r.
(a -> r -> r) -- ^ first arg of 'foldr'
-> r -- ^ second arg of 'foldr'
-> r -- ^ 'foldr' result
}
-- | Convenience function: make a ChurchList out of a regular list
toChurchList :: [a] -> ChurchList a
toChurchList xs = ChurchList (\kons knil -> foldr kons knil xs)
-- | 'toChurchList' isn't actually needed, however, we can make do without '[]'
-- completely.
cons :: a -> ChurchList a -> ChurchList a
cons x xs = ChurchList (\f z -> f x (runlist xs f z))
nil :: ChurchList a
nil = ChurchList (\f z -> z)
foldr' :: (a -> r -> r) -> r -> ChurchList a -> r
foldr' f z xs = runList xs f z
head :: ChurchList a -> Maybe a
head = foldr' ((Just .) . const) Nothing
append :: ChurchList a -> ChurchList a -> ChurchList a
append xs ys = foldr' cons ys xs
-- | Convert a 'ChurchList' to a regular list.
fromChurchList :: ChurchList a -> [a]
fromChurchList xs = runList xs (:) []
_
演習として、他の型をこのように記述してみてください(これは、RankNTypes
拡張機能を使用します— 入門用にこれを読んでください )。この手法は チャーチエンコーディング と呼ばれ、実際のプログラミングで役立つ場合があります。たとえば、GHCはfoldr
/build
fusionと呼ばれるものを使用してリストコードを最適化し、中間リストを削除します。 このHaskell Wikiページ を参照して、build
のタイプに注意してください。
_build :: (forall b. (a -> b -> b) -> b -> b) -> [a]
build g = g (:) []
_
newtype
を除いて、これは上記のfromChurchList
と同じです。基本的に、GHCがリスト処理コードを最適化するために使用するルールの1つは次のとおりです。
_-- Don't materialize the list if all we're going to do with it is
-- fold it right away:
foldr kons knil (fromChurchList xs) ==> runChurchList xs kons knil
_
Churchエンコーディングを内部的に使用する基本的なリスト関数を実装し、その定義を積極的にインライン化し、このルールをインライン化されたコードに適用することにより、map
のような関数のネストされた使用をタイトループに融合できます。
実際には、さまざまなタイプの束全体に適用できる、折りたたみの一般的な概念を思い付くことができます。つまり、リストやツリーなどのfold
関数を体系的に定義できます。
このfold
の一般的な概念は、彼のコメントで言及されている@pelotomのカタモフィズムに対応しています。
重要な洞察は、これらのfold
関数がrecursive型に対して定義されていることです。特に:
_data List a = Cons a (List a) | Nil
data Tree a = Branch (Tree a) (Tree a) | Leaf a
_
これらのタイプはどちらも明らかに再帰的です。List
の場合はCons
で、Tree
の場合はBranch
です。
関数のように、固定小数点を使用してこれらの型を書き換えることができます。 fix
の定義を思い出してください:
_fix f = f (fix f)
_
型に非常によく似たものを実際に書くことができますが、追加のコンストラクタラッパーが必要です。
_newtype Fix f = Roll (f (Fix f))
_
fix
が関数の固定小数点を定義するのと同様に、これは関数の固定小数点を定義します。この新しいFix
型を使用して、すべての再帰型を表現できます。
これにより、List
タイプを次のように書き換えることができます。
_data ListContainer a rest = Cons a rest | Nil
type List a = Fix (ListContainer a)
_
基本的に、Fix
を使用すると、ListContainer
sを任意の深さにネストできます。したがって、次のようになります。
_Roll Nil
Roll (Cons 1 (Roll Nil))
Roll (Cons 1 (Roll (Cons 2 (Roll Nil))))
_
これらはそれぞれ_[]
_、_[1]
_および_[1,2]
_に対応します。
ListContainer
がFunctor
であることは簡単です。
_instance Functor (ListContainer a) where
fmap f (Cons a rest) = Cons a (f rest)
fmap f Nil = Nil
_
ListContainer
からList
へのマッピングはかなり自然だと思います。明示的に再帰する代わりに、再帰部分を変数にします。次に、Fix
を使用して、必要に応じてその変数を入力します。
Tree
にも同様のタイプを記述できます。
なぜ私たちは気にするのですか? fold
を使用して記述されたarbitrary型に対してFix
を定義できます。特に:
_fold :: Functor f => (f a -> a) -> (Fix f -> a)
fold h = h . fmap (fold h) . unRoll
where unRoll (Roll a) = a
_
基本的に、すべての折りたたみは、「ロールされた」タイプのレイヤーを一度に1つずつ展開し、そのたびに結果に関数を適用します。この「展開」により、再帰型の折りたたみを定義し、概念をきちんと自然に一般化できます。
リストの例では、次のように機能します。
Roll
を展開してCons
またはNil
を取得しますfmap
。を使用して、残りのリストを再帰します。Nil
の場合、fmap (fold h) Nil = Nil
なので、Nil
を返します。Cons
の場合、fmap
はリストの残りの部分でフォールドを続行します。fold
で終わるネストされた一連のNil
呼び出しを取得します。これは、標準のfoldr
と同様です。次に、2つのフォールド関数のタイプを見てみましょう。まず、foldr
:
_foldr :: (a -> b -> b) -> b -> [a] -> b
_
現在、fold
はListContainer
に特化しています:
_fold :: (ListContainer a b -> b) -> (Fix (ListContainer a) -> b)
_
最初は、これらは完全に異なって見えます。ただし、少しマッサージすると、同じであることを示すことができます。 foldr
の最初の2つの引数は、_a -> b -> b
_とb
です。関数と定数があります。 b
は_() -> b
_と考えることができます。これで、2つの関数__ -> b
_ができました。ここで、__
_は_()
_と_a -> b
_です。人生を簡単にするために、2番目の関数をカレーして_(a, b) -> b
_を与えましょう。これで、Either
を使用してそれらをsingle関数として書き込むことができます。
_Either (a, b) () -> b
_
_f :: a -> c
_と_g :: b -> c
_を指定すると、常に次のように記述できるため、これは当てはまります。
_h :: Either a b -> c
h (Left a) = f a
h (Right b) = g b
_
これで、foldr
を次のように表示できます。
_foldr :: (Either (a, b) () -> b) -> ([a] -> b)
_
(_->
_のように、右結合である限り、括弧を自由に追加できます。)
ListContainer
を見てみましょう。このタイプには、情報を持たないNil
と、Cons
とa
の両方を持つb
の2つのケースがあります。言い換えると、Nil
は_()
_のようなものであり、Cons
は_(a, b)
_のようなものなので、次のように記述できます。
_type ListContainer a rest = Either (a, rest) ()
_
明らかに、これは上記のfoldr
で使用したものと同じです。だから今私たちは持っています:
_foldr :: (Either (a, b) () -> b) -> ([a] -> b)
fold :: (Either (a, b) () -> b) -> (List a -> b)
_
したがって、実際には、型は同型です-同じものを書くための異なる方法!それはかなりクールだと思います。
(補足として、このタイプの推論について詳しく知りたい場合は、チェックしてください 代数的データタイプの代数 、これについての素晴らしい一連のブログ投稿。)
したがって、固定小数点として記述された型のジェネリックfold
を定義する方法を見てきました。リストの場合、これがfoldr
に直接対応することも確認しました。次に、2番目の例であるバイナリツリーを見てみましょう。次のタイプがあります。
_data Tree a = Branch a (Tree a) (Tree a) | Leaf a
_
上記のルールに従って、Fix
を使用してこれを書き換えることができます。再帰部分を型変数に置き換えます。
_data TreeContainer a rest = Branch rest rest | Leaf a
type Tree a = Fix (TreeContainer a)
_
これで、ツリーfold
ができました。
_fold :: (TreeContainer a b -> b) -> (Tree a -> b)
_
元のfoldTree
は次のようになります:
_foldTree :: (b -> b -> b) -> (a -> b) -> Tree a -> b
_
foldTree
は2つの関数を受け入れます。最初にカレー化し、次にEither
を使用して、を1つに結合します。
_foldTree :: (Either (b, b) a -> b) -> (Tree a -> b)
_
また、Either (b, b) a
が_TreeContainer a b
_に同型であることがわかります。ツリーコンテナーには2つのケースがあります。2つのBranch
sを含むb
と、1つのLeaf
を含むa
です。
したがって、これらの折りたたみタイプは、リストの例と同じように同型です。
明確なパターンが出現しています。通常の再帰的なデータ型を前提として、型の非再帰的なバージョンを体系的に作成できます。これにより、型をファンクターの固定小数点として表現できます。これは、これらすべての異なるタイプのfold
関数を機械的に思い付くことができることを意味します-実際、おそらくGHC Genericsなどを使用してプロセス全体を自動化できるでしょう。
ある意味で、これは実際には、異なる型に対して異なるfold
関数がないことを意味します。むしろ、veryポリモーフィックである単一のfold
関数があります。
私は最初に、これらのアイデアをConal Elliottの a talk から完全に理解しました。これはより詳細になり、unfold
の双対であるfold
についても説明します。
この種のものについてももっと深く掘り下げたい場合は、ファンタスティック "Functional Programming with Bananas、Lenses、Envelopes and Barbed Wire"の論文 を読んでください。 。とりわけ、これはフォールドとアンフォールドに対応する「カタモルフィズム」と「アナモルフィズム」の概念を導入します。
また、自分用のプラグを追加することに抵抗がありません:P。ここでEither
を使用する方法と、別のSO回答で 代数 について話すときに使用した方法との間には、興味深い類似点があります。
実際、fold
と代数の間には深いつながりがあります。また、unfold
--前述のfold
の双対は、代数の双対である合同代数に接続されています。重要なアイデアは、代数的データ型が「初期代数」に対応するということです。これは、私の回答の残りで概説するように、フォールドも定義します。
この接続は、fold
の一般的なタイプで確認できます。
_fold :: Functor f => (f a -> a) -> (Fix f -> a)
_
_f a -> a
_という用語は非常によく知られています。 f-代数は次のようなものとして定義されたことを思い出してください:
_class Functor f => Algebra f a where
op :: f a -> a
_
したがって、fold
は次のように考えることができます。
_fold :: Algebra f a => Fix f -> a
_
基本的に、fold
は、代数を使用して定義された構造を「要約」するだけです。
フォールドは、すべてのコンストラクターを関数に置き換えます。
たとえば、foldr cons nil
は、すべての(:)
をcons
に、[]
をnil
に置き換えます。
foldr cons nil ((:) 1 ((:) 2 [])) = cons 1 (cons 2 nil)
ツリーの場合、foldTree branch leaf
はすべてのBranch
をbranch
に、すべてのLeaf
をleaf
に置き換えます。
foldTree branch leaf (Branch (Branch (Leaf 1) (Leaf 2)) (Leaf 3))
= branch (branch (leaf 1) (leaf 2)) (leaf 2)
これが、すべてのフォールドがコンストラクターとまったく同じ型の引数を受け入れる理由です。
foldr :: (a -> list -> list) -> list -> [a] -> list
foldTree :: (tree -> tree -> tree) -> (a -> tree) -> Tree a -> tree
私はこれをフォールドと呼び、Tree
a Foldable
を宣言します。 GHCドキュメントの Foldable
の例 を参照してください。