web-dev-qa-db-ja.com

再帰的に定義されたリストを理解する(zipWithに関するfibs)

私はHaskellを学んでいて、次のコードに出くわしました。

fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

それがどのように機能するかという点で、私は解析に少し問題があります。それはとてもきちんとしていて、これ以上何も必要ないことは理解していますが、Haskellが次のように書いたときにどのようにfibsを「埋める」ことができるかを理解したいと思います。

take 50 fibs

何か助けはありますか?

ありがとう!

67
Frank

内部でどのように機能するかについて少し説明します。まず、Haskellがその値に thunk と呼ばれるものを使用していることを理解する必要があります。サンクは基本的にまだ計算されていない値です-引数0の関数と考えてください。 Haskellが望むときはいつでも、サンクを評価(または部分的に評価)して、それを実際の値に変えることができます。 部分的にだけがサンクを評価する場合、結果の値にはより多くのサンクが含まれます。

たとえば、次の式について考えてみます。

_(2 + 3, 4)
_

通常の言語では、この値は_(5, 4)_としてメモリに保存されますが、Haskellでは_(<thunk 2 + 3>, 4)_として保存されます。そのタプルの2番目の要素を要求すると、2と3を足し合わせることなく、「4」と表示されます。そのタプルの最初の要素を要求した場合にのみ、サンクが評価され、5であることがわかります。

Fibsの場合、再帰的であるため少し複雑ですが、同じアイデアを使用できます。 fibsは引数を取らないため、Haskellは検出されたリスト要素を永続的に保存します。これは重要です。

_fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
_

fibs、_tail fibs_、およびzipWith (+) fibs (tail fibs)の3つの式に関するHaskellの現在の知識を視覚化するのに役立ちます。 Haskellは次のことを知っていると仮定します。

_fibs                         = 0 : 1 : <thunk1>
tail fibs                    = 1 : <thunk1>
zipWith (+) fibs (tail fibs) = <thunk1>
_

2番目の行は左にシフトされた最初の行であり、3番目の行は合計された最初の2つの行であることに注意してください。

_take 2 fibs_を要求すると、_[0, 1]_が得られます。 Haskellはこれを見つけるために上記をさらに評価する必要はありません。

_take 3 fibs_を要求すると、Haskellは0と1を取得し、サンクを部分的に評価する必要があることに気付きます。 zipWith (+) fibs (tail fibs)を完全に評価するには、最初の2行を合計する必要があります。完全に評価することはできませんが、beginで合計することはできます。最初の2行:

_fibs                         = 0 : 1 : 1: <thunk2>
tail fibs                    = 1 : 1 : <thunk2>
zipWith (+) fibs (tail fibs) = 1 : <thunk2>
_

3行目に「1」を入力すると、3行すべてが同じサンクを共有しているため(書き込まれたポインターのように考えてください)、1行目と2行目にも自動的に表示されることに注意してください。また、評価が終了しなかったため、必要に応じて、リストのrestを含む新しいサンクを作成しました。

ただし、_take 3 fibs_が実行されるため、これは必要ありません:_[0, 1, 1]_。しかし今、あなたが_take 50 fibs_を要求するとします。 Haskellはすでに0、1、1を覚えています。しかしそれは続ける必要があります。したがって、最初の2行を合計し続けます。

_fibs                         = 0 : 1 : 1 : 2 : <thunk3>
tail fibs                    = 1 : 1 : 2 : <thunk3>
zipWith (+) fibs (tail fibs) = 1 : 2 : <thunk3>
_

.。

_fibs                         = 0 : 1 : 1 : 2 : 3 : <thunk4>
tail fibs                    = 1 : 1 : 2 : 3 : <thunk4>
zipWith (+) fibs (tail fibs) = 1 : 2 : 3 : <thunk4>
_

以下同様に、3行目の48列に入力され、最初の50個の数値が計算されるまで続きます。 Haskellは必要なだけ評価し、シーケンスの無限の「残り」をサンクオブジェクトとして残します。

その後_take 25 fibs_を要求した場合、Haskellはそれを再度評価しないことに注意してください-すでに計算したリストから最初の25個の数値を取得するだけです。

編集:混乱を避けるために各サンクに一意の番号を追加しました。

112
mgiuca

しばらく前にこれについて記事を書きました。あなたはそれを見つけることができます ここ

そこで述べたように、PaulHudakの著書「TheHaskellSchool of Expression」の14.2章を読んでください。ここでは、フィボナッチの例を使用して、再帰ストリームについて説明しています。

注:シーケンスのテールは、最初の項目のないシーケンスです。

 | --- + --- + --- + --- + ---- + ---- + ---- + ---- + -------- ---------------------------- | 
 | 1 | 1 | 2 | 3 | 5 | 8 | 13 | 21 |フィボナッチ数列(fibs)| 
 | --- + --- + --- + --- + ---- + ---- + ---- + ---- +- ---------------------------------- | 
 | 1 | 2 | 3 | 5 | 8 | 13 | 21 | 34 |フィボナッチ数列のテール(テールフィボナッチ)| 
 | --- + --- + --- + --- + ---- + ---- + ---- + ---- + ------------------------------------ | 

2つの列を追加します:fibs(tail fibs)を追加して、fibシーケンスのテールのtailを取得します

 | --- + --- + --- + --- + ---- + ---- + ---- + ---- + -------- ---------------------------- | 
 | 2 | 3 | 5 | 8 | 13 | 21 | 34 | 55 |フィボナッチ数列のテールのテール| 
 | --- + --- + --- + --- + ---- + ---- + ---- + ---- +- ----------------------------------- | 

add fibs(tail fibs)はzipWith(+)fibs(tail fibs)と書くことができます

これで、完全なフィボナッチ数列を取得するために、最初の2つのフィボナッチ数から始めて生成を素数化する必要があります。

1:1:zipWith(+)フィブ(テールフィブ)

注:この再帰的定義は、先行評価を行う一般的な言語では機能しません。遅延評価を行うので、haskellで機能します。したがって、最初の4つのフィボナッチ数4つのフィボナッチ数を要求すると、haskellは必要に応じて十分な数のシーケンスのみを計算します。

21
Babu Srinivasan

実行された非常に関連する例を見つけることができます ここ 、私はそれを完全に調べていませんが、それはおそらくいくつかの助けになるでしょう。

実装の詳細は正確にはわかりませんが、以下の私の議論の行にあるべきだと思います。

これを少し塩と一緒に取ってください。これは実装上不正確かもしれませんが、理解の助けとしてだけです。

Haskellは、それ自体が美しい概念である遅延評価として知られている、強制されない限り何も評価しません。

したがって、take 3 fibsを実行するように要求されたと仮定します。Haskellはfibsリストを0:1:another_listとして格納します。take 3を要求されたため、fibs = 0:1:x:another_listおよび(tail fibs) = 1:x:another_listおよび0 : 1 : zipWith (+) fibs (tail fibs)0 : 1 : (0+1) : (1+x) : (x+head another_list) ...になります

パターンマッチングにより、Haskellはx = 0 + 1を知っているので、0:1:1に導きます。

誰かが適切な実装の詳細を知っていれば、私は本当に興味があります。ただし、遅延評価手法はかなり複雑になる可能性があることは理解できます。

これが理解に役立つことを願っています。

再度必須の免責事項:これをほんの少しの塩で取ってください。これは実装上不正確かもしれませんが、理解の助けとしてだけです。

3
Afroz

zipWithzipWith f (x:xs) (y:ys) = f x y : zipWith xs ysの定義を見てみましょう。

私たちのfibsは:fibs = 0 : 1 : zipWith (+) fibs (tail fibs)

_take 3 fibs_をzipWithxs = tail (x:xs)の定義に置き換えると、0 : 1 : (0+1) : zipWith (+) (tail fibs) (tail (tail fibs))が得られます。

_take 4 fibs_をもう一度代入すると、0 : 1 : 1 : (1+1) : zipWith (+) (tail (tail fibs)) (tail (tail (tail fibs)))が得られます。

等々。

1
ArthurVard