web-dev-qa-db-ja.com

foldrを使用してfoldlを書き込む

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
_

しかし、それでも完全には理解できません。ここに私の質問があります。

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

いくつかの説明は整理されています!

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

この演算子を使用すると、sumlsuml = 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を再定義することはできません。 foldfoldlに関する有用な「双対定理」、および特定のアプリケーションに最適な演算子を決定するためのいくつかのガイドラインがあります(Bird、1998)。

フォルダのプロトタイプはフォルダです::(a-> b-> b)-> b-> [a]-> b

Haskellプログラマーは、foldrtype(a -> b -> b) -> b -> [a] -> bであると言います。

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

これは紛らわしくて魔法です!トリックを実行し、アキュムレータを関数に置き換えます。関数は、初期値に適用されて結果を生成します。

グラハムハットンは、上記の記事で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の簡約演算子(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 の普遍的なプロパティを使用します。


参照

92
Don Stewart

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に適用されます。

9
C. A. McCann

(私の回答をすばやく読み飛ばして [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 xssum (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
_

練習問題:

  1. 上記の left の関数と同じように、mapfilterreverseconcatconcatMapを再帰的に実装します。

  2. 上記の式に従って、これらの5つの関数をfoldrに変換します。つまり、右側の折りたたみ式でfvを置き換えます

FoldrによるFoldl

数値を左から右に合計する再帰関数を作成するにはどうすればよいですか?

_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 の普遍性と表現力に関するチュートリアルです。いくつかのペンと紙を手に入れ、自分で折り目のほとんどを導き出すまで、彼が書いたすべてのものを理解してみてください。何かが分からなくても汗をかかないでください。いつでも後で戻ることができますが、あまり先延ばしにすることもありません。

5

foldlfoldrで表すことができるという私の証明は次のとおりです。これは、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に適用されます。 myfoldlfoldlであるかどうかを調べるために調査する2つのケースがあります。

  1. 基本ケース:空のリスト

      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)
    
  2. 空でないリスト

      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

4
David

数学への王道も、ハスケルを通すことすらありません。しましょう

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]

王道ですか?

1
disznoperzselo

これは役立つかもしれません。別の方法で拡大してみました。

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 = ...
1
Dulguun Otgon
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)

まあ、少なくとも、これは私を助けました。それでもまったく正しくありません。

0
hanrai