1. ホーム
  2. haskell

[解決済み] foldrを使ったfoldlの書き方

2023-08-01 20:14:14

質問

実世界のハスケル 第4章をご覧ください。 関数型プログラミング :

foldrでfoldlを書く。

-- file: ch04/Fold.hs
myFoldl :: (a -> b -> a) -> a -> [b] -> a

myFoldl f z xs = foldr step id xs z
    where step x g a = g (f a x)

上記のコードは私を大いに混乱させ、誰かが dps が意味のある名前で書き直してくれたので、少しは分かりやすくなりました。

myFoldl stepL zeroL xs = (foldr stepR id xs) zeroL
where stepR lastL accR accInitL = accR (stepL accInitL lastL)

Jef Gは、例を示し、基礎となるメカニズムを段階的に示すことで、素晴らしい仕事をしました。

myFoldl (+) 0 [1, 2, 3]
= (foldR step id [1, 2, 3]) 0
= (step 1 (step 2 (step 3 id))) 0
= (step 1 (step 2 (\a3 -> id ((+) a3 3)))) 0
= (step 1 (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2))) 0
= (\a1 -> (\a2 -> (\a3 -> id ((+) a3 3)) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (\a3 -> (+) a3 3) ((+) a2 2)) ((+) a1 1)) 0
= (\a1 -> (\a2 -> (+) ((+) a2 2) 3) ((+) a1 1)) 0
= (\a1 -> (+) ((+) ((+) a1 1) 2) 3) 0
= (+) ((+) ((+) 0 1) 2) 3
= ((0 + 1) + 2) + 3

しかし、私はまだそれを完全に理解することはできません、以下は私の質問です。

  1. id 関数は何のためにあるのですか。何のための役割なのでしょうか?なぜここでそれが必要なのでしょうか?
  2. 上記の例では、id関数はラムダ関数内のアキュムレータですか?
  3. foldrのプロトタイプは foldr :: (a -> b -> b) -> b -> [a] -> b で、最初のパラメータは2つのパラメータを必要とする関数ですが、myFoldlの実装ではステップ関数が3つのパラメータを使用しています。

どのように解決するのですか?

いくつかの説明が必要です。

id機能は何のためにあるのでしょうか?何のための役割なのでしょうか?なぜここでそれが必要なのでしょうか?

id アイデンティティ機能 , id x = x で関数の連鎖を構築する際にゼロと等価なものとして使用されます。 関数構成 , (.) . あなたはそれを見つけることができます で定義されている、前奏曲の .

上記の例では、id関数はラムダ関数内のアキュムレータ?

アキュムレータは、関数の適用を繰り返すことで構築されていく関数です。アキュムレータに名前を付けているので、明示的なラムダはありません。 step . 必要ならラムダを付けて書いてもいい。

foldl f a bs = foldr (\b g x -> g (f x b)) id bs a

または グラハム・ハットンならこう書くだろう :

<ブロッククオート

5.1節 foldl 演算子

では、一般化して suml の例から一般化して、標準的な演算子 foldl で、リストの要素を左から右の順に処理する、関数 f を使って値を組み合わせ、さらに値 v を開始値として使用します。

foldl :: (β → α → β) → β → ([α] → β)
foldl f v [ ] = v
foldl f v (x : xs) = foldl f (f v x) xs

この演算子を使って suml は単に suml = foldl (+) 0 . 他の多くの関数も,簡単な方法で foldl . 例えば,標準的な関数 reverse を使って再定義することができます。 foldl を使って次のように再定義します。

reverse :: [α] → [α]
reverse = foldl (λxs x → x : xs) [ ]

この定義は、非効率的な append 演算子の使用を避けるため、fold を使用したオリジナルの定義よりも効率的です。 (++) を使用しないため、リストに対してより効率的です。

前節の計算を単純に一般化したもので、関数 suml を再定義する方法を示します。 foldl の観点から fold :

foldl f v xs = fold (λx g → (λa → g (f a x))) id xs v

これに対して,再定義された fold の観点から foldl という事実のために foldl はリスト引数の末尾が厳密ですが fold はそうではない。に関する多くの有用な「二元性定理」があります。 foldfoldl といった演算子、そして特定のアプリケーションに最も適した演算子を決定するためのいくつかのガイドラインがあります (Bird, 1998)。

foldrのプロトタイプはfoldr :: (a -> b -> b) -> b -> [a] -> b

Haskellのプログラマなら foldr(a -> b -> b) -> b -> [a] -> b .

で、最初のパラメータは2つのパラメータを必要とする関数ですが、myFoldlの実装ではステップ関数が3つのパラメータを使用しているので、私は完全に混乱しています。

これは紛らわしくて不思議ですね

これは混乱するようで不思議です!トリックを施し、アキュムレータを関数に置き換え、それを初期値に適用して結果を得ます。

Graham Huttonは、次のようにトリックを説明します。 foldlfoldr に変換する。の再帰的な定義を書き下すことから始めます。 foldl :

foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v []       = v
foldl f v (x : xs) = foldl f (f v x) xs

の静的引数変換でリファクタリングします。 f :

foldl :: (a -> b -> a) -> a -> [b] -> a    
foldl f v xs = g xs v
    where
        g []     v = v
        g (x:xs) v = g xs (f v x)

では、次のように書き換えてみましょう。 g を浮き上がらせるように v を内側に浮かせる。

foldl f v xs = g xs v
    where
        g []     = \v -> v
        g (x:xs) = \v -> g xs (f v x)

と考えるのと同じです。 g を関数を返す1つの引数の関数として考えるのと同じです。

foldl f v xs = g xs v
    where
        g []     = id
        g (x:xs) = \v -> g xs (f v x)

ここで g のように、リストを再帰的に走査する関数に、ある関数を適用します。 f . 最終的な値は恒等式関数で、各ステップの結果も同様に関数になります。

しかし を使えば、リストに対する非常に似た再帰的な関数が既に手元にあります。 foldr !

2 fold演算子

この演算子は fold 演算子は再帰理論(Kleene, 1952)に起源を持ち、一方で の fold をプログラミング言語の中心概念として使うようになったのは、APLのreduction演算子(Iverson, 1962)、そして後にFPのinsertion演算子(Backus, 1978). Haskell では fold 演算子は次のように定義できる。

fold :: (α → β → β) → β → ([α] → β)
fold f v [ ] = v
fold f v (x : xs) = f x (fold f v xs)

つまり、ある関数が与えられたとき f 型の α → β → β という型と、値 v 型の β である場合、関数 fold f v 型のリストを処理します。 [α] 型の値を与えるために β という型の値を与える。 コンストラクタを [] をリストの末尾にある値 v であり、各コンストラクタは (:) という関数によってリスト内の f . このように fold 演算子はリストを処理するための単純な再帰パターンをカプセル化します。この場合、リストに対する2つのコンストラクタは単に他の値や関数に置き換えられます。リストに関する多くのよく知られた関数は fold .

これは、非常によく似た再帰的なスキームのように見えます。 g 関数と非常によく似た再帰的スキームのように見えます。手近なマジック(Bird、Meertens、Malcolm)をすべて使って、特別なルールである 折り畳みの普遍的性質 という特別なルールを適用する。 g の2つの定義間の等価性である。

g [] = v
g (x:xs) = f x (g xs)

もし

g = fold f v

つまり、折り畳みの普遍的な性質は、次のように述べています。

    g = foldr k v

ここで g は二つの方程式と等価でなければなりません。 kv :

    g []     = v
    g (x:xs) = k x (g xs)

先ほどのfoldlの設計から、私たちは v == id . しかし、2番目の式では になります。 を計算する。 の定義は k :

    g (x:xs)         = k x (g xs)        
<=> g (x:xs) v       = k x (g xs) v      -- accumulator of functions
<=> g xs (f v x)     = k x (g xs) v      -- definition of foldl
<=  g' (f v x)       = k x g' v          -- generalize (g xs) to g'
<=> k = \x g' -> (\a -> g' (f v x))      -- expand k. recursion captured in g'

という計算された定義を代入すると kv は というfoldlの定義が得られます。

foldl :: (a -> b -> a) -> a -> [b] -> a    
foldl f v xs =
    foldr
        (\x g -> (\a -> g (f v x)))
        id
        xs
        v

再帰的な g はfoldrコンビネータに置き換えられ、アキュムレータは f の連鎖によって構築される関数になります(右折りではなく左折り)。

これは確かにやや高度なので、この変換を深く理解するために 折り返しの普遍的な性質 を深く理解するには、以下のリンクにある Hutton のチュートリアルをお勧めします。


参考文献