Learn You A Haskell からの次の文章に問題があります。
大きな違いの1つは、右側のフォールドが無限リストで機能するのに対し、左側のフォールドは機能しないことです。端的に言えば、ある時点で無限のリストを取得し、それを右から折りたたむと、最終的にリストの先頭に到達します。ただし、ある時点で無限のリストを取得し、それを左から折りたたむと、終わりに達することはありません。
わからない。無限遠点を取り、それを右から折りたたむと、無限遠点から開始する必要がありますが、これは発生していません(これを実行できる言語を誰かが知っている場合は、次のように伝えてください:p )。少なくとも、Haskellの実装に従ってそこから始める必要があります。Haskellではfoldrとfoldlは、リストのどこから折り畳みを開始するかを決定する引数をとらないからです。
あなたが無限リストを取り、定義されたインデックスから右折りたたみを開始する場合、それが意味があるので、foldrとfoldlがリストのどこで折りたたみを開始するかを決定する引数を取った場合の引用に同意します willは最終的に終了しますが、左の折り目からどこから始めてもかまいません。あなたは無限に向かって折りたたまれます。ただし、foldrとfoldlはこの引数を取りません。したがって、引用は意味がありません。 Haskellでは、無限リストの左折りと右折りの両方が終了しません。
私の理解は正しいですか、それとも何かが足りませんか?
ここでの鍵は怠惰です。リストを折りたたむために使用している関数が厳密である場合、リストが無限であるとすると、左折りも右折りも終了しません。
Prelude> foldr (+) 0 [1..]
^CInterrupted.
ただし、それほど厳密でない関数を折りたたむと、終了結果が得られる可能性があります。
Prelude> foldr (\x y -> x) 0 [1..]
1
無限のデータ構造である結果を取得することもできます。そのため、ある意味では終了しない一方で、遅延して消費される可能性のある結果を生成することができます。
Prelude> take 10 $ foldr (:) [] [1..]
[1,2,3,4,5,6,7,8,9,10]
ただし、これはfoldl
では機能しません。遅延の有無にかかわらず、最も外側の関数呼び出しを評価することはできないからです。
Prelude> foldl (flip (:)) [] [1..]
^CInterrupted.
Prelude> foldl (\x y -> y) 0 [1..]
^CInterrupted.
左と右の折りたたみの主な違いは、リストがトラバースされる順序ではなく、常に左から右であることに注意してください。結果の関数アプリケーションがどのようにネストされるかは異なります。
foldr
を使用すると、それらは「内部」にネストされます
foldr f y (x:xs) = f x (foldr f y xs)
ここで、最初の反復はf
の最も外側のアプリケーションになります。したがって、f
は遅延する可能性があるため、2番目の引数が常に評価されるとは限らないか、2番目の引数を強制せずにデータ構造の一部を生成できます。
foldl
を使用すると、「外側」にネストされます
foldl f y (x:xs) = foldl f (f y x) xs
ここでは、f
が厳密であるかどうかに関係なく、無限リストの場合には到達できないf
の最も外側のアプリケーションに到達するまで、何も評価できません。
キーフレーズは「ある時点で」です。
無限リストある時点でを取り、それを右から畳むと、最終的にリストの先頭に到達します。
つまり、無限リストの「最後の」要素から始めることはできないでしょう。しかし、著者のポイントはこれです。遠く離れたところにある点(エンジニアの場合、これは無限に「十分に近い」)を選択して、左に折りたたみを開始します。最終的には、リストの先頭に移動します。同じことは左の折り目には当てはまりません。そこにあるポイントを選んで(そしてそれをリストの先頭に「十分に近い」と呼んで)、右に折りたたむと、まだ無限の道があります。
つまり、秘訣は、無限に行く必要がない場合があるということです。あなたもそこに行く必要はないかもしれません。ただし、事前にどこまで進む必要があるかわからない場合は、無限のリストが非常に便利です。
簡単な図はfoldr (:) [] [1..]
です。フォールドをしましょう。
foldr f z (x:xs) = f x (foldr f z xs)
であることを思い出してください。無限のリストでは、実際にはz
が何であるかは問題ではないので、図を乱雑にする[]
ではなくz
として保持しています。
foldr (:) z (1:[2..]) ==> (:) 1 (foldr (:) z [2..])
1 : foldr (:) z (2:[3..]) ==> 1 : (:) 2 (foldr (:) z [3..])
1 : 2 : foldr (:) z (3:[4..]) ==> 1 : 2 : (:) 3 (foldr (:) z [4..])
1 : 2 : 3 : ( lazily evaluated thunk - foldr (:) z [4..] )
理論的にはrightからのフォールドであるにもかかわらず、foldr
がどのように生成されるかを確認してください。この場合、結果のリストの個々の要素は、 左?したがって、このリストからtake 3
を取得すると、[1,2,3]
を生成でき、折り目をさらに評価する必要がないことが明確にわかります。
Haskellでは、遅延評価のために無限リストを使用できることを忘れないでください。したがって、 `[1 ..]は無限に長いのですが、_head [1..]
_は1であり、head $ map (+1) [1..]
は2です。それがわからない場合は、停止してしばらく遊んでください。それが得られたら、読み続けてください...
混乱の一部は、foldl
とfoldr
が常にどちらかの側から始まるため、長さを指定する必要がないことだと思います。
foldr
の定義は非常に単純です
_ foldr _ z [] = z
foldr f z (x:xs) = f x $ foldr f z xs
_
なぜこれが無限リストで終了するのか、よく試してください
_ dumbFunc :: a -> b -> String
dumbFunc _ _ = "always returns the same string"
testFold = foldr dumbFunc 0 [1..]
_
ここでは、foldr
a ""(値は重要ではないため)と自然数の無限リストを渡します。これは終了しますか?はい。
それが終了する理由は、Haskellの評価が遅延項書き換えと同等であるためです。
そう
_ testFold = foldr dumbFunc "" [1..]
_
になる(パターンマッチングを可能にするため)
_ testFold = foldr dumbFunc "" (1:[2..])
_
これは(フォールドの定義から)と同じです
_ testFold = dumbFunc 1 $ foldr dumbFunc "" [2..]
_
dumbFunc
の定義により、次のように結論付けることができます。
_ testFold = "always returns the same string"
_
これは、何かを実行する関数があるが、怠惰な場合がある場合に、より興味深いものになります。例えば
_foldr (||) False
_
リストにTrue
要素が含まれているかどうかを検索するために使用されます。これを使用して、高次関数を定義できますany
は、渡された関数がリストのいくつかの要素に対してtrueの場合にのみTrue
を返します
_any :: (a -> Bool) -> [a] -> Bool
any f = (foldr (||) False) . (map f)
_
遅延評価の良いところは、最初の要素e
が_f e == True
_になると、これが停止することです。
一方、これはfoldl
には当てはまりません。どうして?本当に単純なfoldl
は次のようになります
_foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xs
_
さて、上の例を試してみたらどうなるでしょう。
_testFold' = foldl dumbFunc "" [1..]
testFold' = foldl dumbFunc "" (1:[2..])
_
これは今:
_testFold' = foldl dumbFunc (dumbFunc "" 1) [2..]
_
そう
_testFold' = foldl dumbFunc (dumbFunc (dumbFunc "" 1) 2) [3..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) [4..]
testFold' = foldl dumbFunc (dumbFunc (dumbFunc (dumbFunc (dumbFunc "" 1) 2) 3) 4) [5..]
_
などなど。 Haskellは常に最も外側の関数を最初に評価するため(つまり、一言で言えば遅延評価)、どこにも到達することはできません。
これのクールな結果の1つは、foldl
からfoldr
を実装できることですが、その逆はできません。これは、ある意味でfoldr
は、他のほとんどすべての実装に使用するものであるため、すべての高次文字列関数の最も基本的なものであることを意味します。 canfoldl
末尾を再帰的に実装し、そこからパフォーマンスを向上させることができるため、foldl
を使用することもできます。
Haskell wiki にはわかりやすい説明があります。さまざまなタイプのフォールド関数とアキュムレーター関数を使用した段階的な削減を示しています。