Real World Haskellの第4章 Functional Programming について:
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
_
しかし、それでも完全には理解できません。ここに私の質問があります。
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
または Graham Huttonが書きます :
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) [ ]
この定義は、foldを使用した元の定義よりも効率的です。これは、リストに対して非効率な追加演算子
(++)
を使用しないためです。前のセクションの関数
suml
の計算の簡単な一般化は、foldl
に関して関数fold
を再定義する方法を示しています。foldl f v xs = fold (λx g → (λa → g (f a x))) id xs v
対照的に、
fold
はリスト引数の末尾で厳密であるがfoldl
は厳密ではないため、foldl
に関してfold
を再定義することはできません。fold
とfoldl
に関する有用な「双対定理」、および特定のアプリケーションに最適な演算子を決定するためのいくつかのガイドラインがあります(Bird、1998)。
フォルダのプロトタイプはフォルダです::(a-> b-> b)-> b-> [a]-> b
Haskellプログラマーは、foldr
のtypeは(a -> b -> b) -> b -> [a] -> b
であると言います。
最初のパラメーターは2つのパラメーターを必要とする関数ですが、myFoldlの実装のステップ関数は3つのパラメーターを使用しています。完全に混乱しています
これは紛らわしくて魔法です!トリックを実行し、アキュムレータを関数に置き換えます。関数は、初期値に適用されて結果を生成します。
グラハムハットンは、上記の記事でfoldl
をfoldr
に変換するコツを説明しています。最初に、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の簡約演算子(Iverson、1962)にさかのぼります。 FP(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
は、タイプ[α]
のリストを処理して、リストの末尾にあるnilコンストラクタβ
を値v
で置き換えることにより、タイプ[]
の値を取得します、および関数f
によるリスト内の各consコンストラクタ(:)
。このようにして、fold
演算子は、リストを処理するための単純な再帰パターンをカプセル化します。リストの2つのコンストラクターは、他の値と関数に置き換えられるだけです。リストのよく知られた関数の多くは、fold
を使用した簡単な定義を持っています。
これは、g
関数と非常によく似た再帰的なスキームのように見えます。さて、トリック:手元にあるすべての魔法(別名Bird、Meertens、Malcolm)を使用して、特別なルールfold of universal propertyを適用します。これは、関数g
の2つの定義間の同値ですプロセスリスト、次のように記述
g [] = v g (x:xs) = f x (g xs)
もしそうなら
g = fold f v
したがって、フォールドの普遍的な特性は次のように述べています。
g = foldr k v
ここで、g
は、一部のk
およびv
の2つの方程式と同等でなければなりません。
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'
計算されたk
およびv
の定義を代入すると、foldlの定義は次のようになります。
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f v xs =
foldr
(\x g -> (\a -> g (f v x)))
id
xs
v
再帰的なg
はフォルダーコンビネーターに置き換えられ、アキュムレーターはリストの各要素で逆の順序でf
の構成のチェーンを介して構築された関数になります(したがって、右ではなく左にフォールドします)。
これは確かにある程度進んでいるため、この変換を深く理解するには、変換を可能にする folder の普遍的なプロパティを使用します。
参照
foldr
のタイプを検討してください:
_foldr :: (b -> a -> a) -> a -> [b] -> a
_
一方、step
の型はb -> (a -> a) -> a -> a
のようなものです。ステップがfoldr
に渡されているので、この場合、フォールドには_(b -> (a -> a) -> (a -> a)) -> (a -> a) -> [b] -> (a -> a)
_のようなタイプがあると結論付けることができます。
異なるシグネチャにおけるa
の異なる意味に混同しないでください。それは単なる型変数です。また、関数の矢印は右に関連付けられているため、_a -> b -> c
_はa -> (b -> c)
と同じです。
したがって、はい、foldr
のアキュムレータ値は_a -> a
_タイプのfunctionであり、初期値はid
です。 id
は何もしない関数であるため、これはある程度理にかなっています。リストのすべての値を追加するときに、初期値としてゼロから始めるのと同じ理由です。
step
が3つの引数を取る場合は、次のように書き直してください。
_step :: b -> (a -> a) -> (a -> a)
step x g = \a -> g (f a x)
_
何が起こっているのか簡単に確認できますか?関数を返すため、追加のパラメーターが必要であり、2つの書き込み方法は同等です。 foldr
の後の追加パラメーターにも注意してください:_(foldr step id xs) z
_。括弧内の部分は折り畳み自体であり、関数を返し、z
に適用されます。
(私の回答をすばやく読み飛ばして [1] 、 [2] 、 [3] 、 [4] to Haskellの構文、高階関数、カリー化、関数構成、$演算子、中置/前置演算子、セクション、ラムダを必ず理解してください)
fold は、特定の種類の再帰をコード化したものです。また、普遍性プロパティは、再帰が特定の形式に準拠している場合、いくつかの正式なルールに従って折りたたみに変換できることを示しています。逆に、すべての折り畳みをそのような再帰に変換できます。繰り返しになりますが、一部の再帰はまったく同じ答えを与えるフォールドに変換できますが、一部の再帰は変換できません。そのための正確な手順があります。
基本的に、再帰関数が left のようなリストで機能する場合、 right を折りたたみ、f
に置き換えることができますそして実際に何があるかについてはv
。
_g [] = v ⇒
g (x:xs) = f x (g xs) ⇒ g = foldr f v
_
例えば:
_sum [] = 0 {- recursion becomes fold -}
sum (x:xs) = x + sum xs ⇒ sum = foldr 0 (+)
_
ここで_v = 0
_およびsum (x:xs) = x + sum xs
はsum (x:xs) = (+) x (sum xs)
と同等であるため、f = (+)
となります。さらに2つの例
_product [] = 1
product (x:xs) = x * product xs ⇒ product = foldr 1 (*)
length [] = 0
length (x:xs) = 1 + length xs ⇒ length = foldr (\_ a -> 1 + a) 0
_
練習問題:
上記の left の関数と同じように、
map
、filter
、reverse
、concat
、concatMap
を再帰的に実装します。上記の式に従って、これらの5つの関数をfoldrに変換します。つまり、右側の折りたたみ式で
f
とv
を置き換えます。
数値を左から右に合計する再帰関数を作成するにはどうすればよいですか?
_sum [] = 0 -- given `sum [1,2,3]` expands into `(1 + (2 + 3))`
sum (x:xs) = x + sum xs
_
検索を開始する最初の再帰関数は、合計を開始する前に完全に拡張されますが、それは必要ありません。 1つのアプローチは、 accumulator を含む再帰関数を作成することです。これは、各ステップですぐに数値を加算します(再帰戦略の詳細については tail recursion を参照) :
_suml :: [a] -> a
suml xs = suml' xs 0
where suml' [] n = n -- auxiliary function
suml' (x:xs) n = suml' xs (n+x)
_
よし、やめろ!このコードをGHCiで実行し、コードがどのように機能するかを理解していることを確認してから、慎重かつ慎重に続行してください。 suml
はフォールドで再定義できませんが、_suml'
_は再定義できます。
_suml' [] = v -- equivalent: v n = n
suml' (x:xs) n = f x (suml' xs) n
_
_suml' [] n = n
_関数定義からですよね?そして、普遍的なプロパティ式からの_v = suml' []
_。これにより、_v n = n
_が得られます。この関数は、受け取ったものをすぐに返します:_v = id
_。 f
を計算してみましょう:
_suml' (x:xs) n = f x (suml' xs) n
-- expand suml' definition
suml' xs (n+x) = f x (suml' xs) n
-- replace `suml' xs` with `g`
g (n+x) = f x g n
_
したがって、suml' = foldr (\x g n -> g (n+x)) id
、つまりsuml = foldr (\x g n -> g (n+x)) id xs 0
となります。
_foldr (\x g n -> g (n + x)) id [1..10] 0 -- return 55
_
ここで、一般化し、_+
_を変数関数に置き換えるだけです。
_foldl f a xs = foldr (\x g n -> g (n `f` x)) id xs a
foldl (-) 10 [1..5] -- returns -5
_
グラハムハットンの チュートリアルを読んでください。fold の普遍性と表現力に関するチュートリアルです。いくつかのペンと紙を手に入れ、自分で折り目のほとんどを導き出すまで、彼が書いたすべてのものを理解してみてください。何かが分からなくても汗をかかないでください。いつでも後で戻ることができますが、あまり先延ばしにすることもありません。
foldl
がfoldr
で表すことができるという私の証明は次のとおりです。これは、step
関数が導入するスパゲッティという名前とは別に、非常に単純です。
命題は、foldl f z xs
が
myfoldl f z xs = foldr step_f id xs z
where step_f x g a = g (f a x)
ここで最初に注意する重要なことは、最初の行の右側が実際には次のように評価されることです。
(foldr step_f id xs) z
foldr
は3つのパラメータのみを取るためです。これは、foldr
が値ではなくカリー関数を計算することをすでに示唆しており、それがz
に適用されます。 myfoldl
がfoldl
であるかどうかを調べるために調査する2つのケースがあります。
基本ケース:空のリスト
myfoldl f z []
= foldr step_f id [] z (by definition of myfoldl)
= id z (by definition of foldr)
= z
foldl f z []
= z (by definition of foldl)
空でないリスト
myfoldl f z (x:xs)
= foldr step_f id (x:xs) z (by definition of myfoldl)
= step_f x (foldr step_f id xs) z (-> apply step_f)
= (foldr step_f id xs) (f z x) (-> remove parentheses)
= foldr step_f id xs (f z x)
= myfoldl f (f z x) xs (definition of myfoldl)
foldl f z (x:xs)
= foldl f (f z x) xs
2.では、最初の行と最後の行はどちらの場合も同じ形式であるため、xs == []
までリストを折りたたむのに使用できます。この場合、1。は同じ結果を保証します。したがって、誘導により、myfoldl == foldl
。
数学への王道も、ハスケルを通すことすらありません。しましょう
h z = (foldr step id xs) z where
step x g = \a -> g (f a x)
一体何ですかh z
?と仮定する xs = [x0, x1, x2]
。
フォルダの定義を適用します:
h z = (step x0 (step x1 (step x2 id))) z
ステップの定義を適用します。
= (\a0 -> (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f a0 x0)) z
ラムダ関数に代入します。
= (\a1 -> (\a2 -> id (f a2 x2)) (f a1 x1)) (f z x0)
= (\a2 -> id (f a2 x2)) (f (f z x0) x1)
= id (f (f (f z x0) x1) x2)
Idの定義を適用:
= f (f (f z x0) x1) x2
Foldlの定義を適用します。
= foldl f z [x0, x1, x2]
王道ですか?
これは役立つかもしれません。別の方法で拡大してみました。
myFoldl (+) 0 [1,2,3] =
foldr step id [1,2,3] 0 =
foldr step (\a -> id (a+3)) [1,2] 0 =
foldr step (\b -> (\a -> id (a+3)) (b+2)) [1] 0 =
foldr step (\b -> id ((b+2)+3)) [1] 0 =
foldr step (\c -> (\b -> id ((b+2)+3)) (c+1)) [] 0 =
foldr step (\c -> id (((c+1)+2)+3)) [] 0 =
(\c -> id (((c+1)+2)+3)) 0 = ...
foldr step zero (x:xs) = step x (foldr step zero xs)
foldr _ zero [] = zero
myFold f z xs = foldr step id xs z
where step x g a = g (f a x)
myFold (+) 0 [1, 2, 3] =
foldr step id [1, 2, 3] 0
-- Expanding foldr function
step 1 (foldr step id [2, 3]) 0
step 1 (step 2 (foldr step id [3])) 0
step 1 (step 2 (step 3 (foldr step id []))) 0
-- Expanding step function if it is possible
step 1 (step 2 (step 3 id)) 0
step 2 (step 3 id) (0 + 1)
step 3 id ((0 + 1) + 2)
id (((0 + 1) + 2) + 3)
まあ、少なくとも、これは私を助けました。それでもまったく正しくありません。