web-dev-qa-db-ja.com

リスト以外のタイプのフォールドを構成するものは何ですか?

単一リンクリストを考えてみましょう。それは次のようになります

data List x = Node x (List x) | End

次のようなフォールディング関数を定義するのは自然です

reduce :: (x -> y -> y) -> y -> List x -> y

ある意味で、reduce f x0は、すべてのNodefに、すべてのEndx0に置き換えます。これは、プレリュードがfoldと呼ぶものです。

ここで、単純なバイナリツリーを考えます。

data Tree x = Leaf x | Branch (Tree x) (Tree x)

同様に、次のような関数を定義することは自然です

reduce :: (y -> y -> y) -> (x -> y) -> Tree x -> y

thisの削減にはまったく異なる性質があることに注意してください。リストベースのものは本質的にシーケンシャルですが、この新しいツリーベースのものは、分割統治の感覚をより多く持っています。そこにいくつかのparコンビネータを投入することを想像することもできます。 (リストバージョンのどこにそのようなものを入れますか?)

私の質問:この関数はまだ「フォールド」として分類されていますか、それとも別のものですか? (もしそうなら、それは何ですか?)

基本的に、誰かがフォールディングについて話すときはいつでも、彼らは常にフォールディングについて話しますlists、これは本質的にシーケンシャルです。 「シーケンシャル」がフォールドとは何かの定義の一部であるのか、またはこれがフォールディングの最も一般的に使用される例の単なる偶然の特性であるのかと思います。

64

Tikhonは技術的なものを落としました。私は彼の言ったことから簡略化しようと思う。

「折りたたみ」という用語は、残念ながら、長年にわたってあいまいになり、次の2つのうちのいずれかを意味します。

  1. コレクションをある順序で順番に削減します。 Haskellでは、これがlarsmansが提示するFoldableクラスでの「折りたたみ」の意味です。
  2. あなたが求めた概念:「破壊」(constructingの反対)、その構造に従って代数データ型を「観察」または「排除」。 カタモフィズムとも呼ばれます。

これらの概念の両方を総称的に定義して、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つの方法は次のとおりです。

  1. 特定のタイプのフォールドは、そのタイプの任意の値に含まれるすべての情報を「見る」ことができます。 (これが、型のコンストラクターを使用して、その型の値を完全に「再構築」できる理由です。)
  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のような関数のネストされた使用をタイトループに融合できます。

63
Luis Casillas

すべての機会に折りたたみ

実際には、さまざまなタイプの束全体に適用できる、折りたたみの一般的な概念を思い付くことができます。つまり、リストやツリーなどの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を使用すると、ListContainersを任意の深さにネストできます。したがって、次のようになります。

_Roll Nil
Roll (Cons 1 (Roll Nil))
Roll (Cons 1 (Roll (Cons 2 (Roll Nil))))
_

これらはそれぞれ_[]_、_[1]_および_[1,2]_に対応します。

ListContainerFunctorであることは簡単です。

_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つずつ展開し、そのたびに結果に関数を適用します。この「展開」により、再帰型の折りたたみを定義し、概念をきちんと自然に一般化できます。

リストの例では、次のように機能します。

  1. 各ステップで、Rollを展開してConsまたはNilを取得します
  2. fmap。を使用して、残りのリストを再帰します。
    1. Nilの場合、fmap (fold h) Nil = Nilなので、Nilを返します。
    2. Consの場合、fmapはリストの残りの部分でフォールドを続行します。
  3. 最後に、foldで終わるネストされた一連のNil呼び出しを取得します。これは、標準のfoldrと同様です。

タイプの比較

次に、2つのフォールド関数のタイプを見てみましょう。まず、foldr

_foldr :: (a -> b -> b) -> b -> [a] -> b
_

現在、foldListContainerに特化しています:

_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と、Consaの両方を持つ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つのBranchsを含む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は、代数を使用して定義された構造を「要約」するだけです。

66
Tikhon Jelvis

フォールドは、すべてのコンストラクターを関数に置き換えます。

たとえば、foldr cons nilは、すべての(:)consに、[]nilに置き換えます。

foldr cons nil ((:) 1 ((:) 2 [])) = cons 1 (cons 2 nil)

ツリーの場合、foldTree branch leafはすべてのBranchbranchに、すべてのLeafleafに置き換えます。

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
37

私はこれをフォールドと呼び、Tree a Foldable を宣言します。 GHCドキュメントの Foldableの例 を参照してください。

7
Fred Foo