21

一定時間の追加を除いて、ストリームはリストのように振る舞うように見えることに気付きました。もちろん、一定時間の追加をリストに追加することはそれほど複雑ではなく、DListはまさにそれを行います。

残りの議論では、リストが一定時間追加されるか、単純に興味がないことを前提とします。

私の考えでは、Haskell のリストは単純にストリームとして実装する必要があります。これが当てはまらないためには、次のことが保持される必要があると思います。

  1. リストがストリームよりも優れている場合がありますAND
  2. ストリームがリストよりも優れている場合があります。

私の質問は、上記の 2 つのケースの例は何ですか?

注: この質問の目的上、私が説明した特定の実装で簡単に修正できる省略は無視してください。ここでコア構造の違いをもっと探しています。

追加情報:

私がここで得ていることの一部は、私たちが書いた場合[1..1000000]、Haskell コンパイラ (GHC など) が行うことです。

  1. リストを作成するOR
  2. リストを完全に記述する 1 と 1000000 の 2 つの int を持つオブジェクトを作成します。

ケース (1) の場合、中間リストの作成は不要なパフォーマンス ペナルティのように思われるため、なぜこれを行うのでしょうか?

または、ケース (2) の場合、なぜストリームが必要なのですか?

4

3 に答える 3

17

を書くとき[1..1000000]、GHCが実際に行うことは、関心のあるリストを作成する方法を含み11000000それを説明するオブジェクトを作成することです。そのオブジェクトは「サンク」と呼ばれます。このリストは、ケースの精査者を満足させるために必要な場合にのみ作成されます。たとえば、次のように書くことができます。

printList [] = putStrLn ""
printList (x:xs) = putStrLn (show x) >> printList xs

main = printList [1..1000000]

これは次のように評価されます:

main
= { definition of main }
printList [1..1000000]
= { list syntax sugar }
printList (enumFromTo 1 1000000)
= { definition of printList }
case enumFromTo 1 1000000 of
    [] -> putStrLn ""
    x:xs -> putStrLn (show x) >> printList xs
= { we have a case, so must start evaluating enumFromTo;
    I'm going to skip a few steps here involving unfolding
    the definition of enumFromTo and doing some pattern
    matching }
case 1 : enumFromTo 2 1000000 of
    [] -> putStrLn ""
    x:xs -> putStrLn (show x) >> printList xs
= { now we know which pattern to choose }
putStrLn (show 1) >> printList (enumFromTo 2 1000000)

次に、それがコンソールに印刷されていることがわかります。1上部の近くから、のenumFromTo 2 1000000代わりにを使用して開始しenumFromTo 1 1000000ます。最終的には、すべての数値が印刷され、評価する時期が来るでしょう。

printList (enumFromTo 1000000 1000000)
= { definition of printList }
case enumFromTo 1000000 1000000 of
    [] -> putStrLn ""
    x:xs -> putStrLn (show x) >> printList xs
= { skipping steps again to evaluate enumFromTo }
case [] of
    [] -> putStrLn ""
    x:xs -> putStrLn (show x) >> printList xs
= { now we know which pattern to pick }
putStrLn ""

そして評価は終了します。

ストリームが必要な理由は少し微妙です。元の論文「ストリームの融合:リストからストリーム、そして何もなしまで」には、おそらく最も完全な説明があります。短いバージョンは、長いパイプラインがある場合です。

concatMap foo . map bar . filter pred . break isSpecial

...コンパイラにすべての中間リストをコンパイルさせる方法はそれほど明白ではありません。リストは、反復されている一種の「状態」を持っていると考えることができ、これらの各関数は、リストをトラバースするのではなく、反復ごとに状態が変更される方法を変更しているだけです。タイプはStreamこれを明示的にしようとし、結果はストリーム融合です。外観は次のとおりです。まず、これらすべての関数をストリームバージョンに変換します。

(toList . S.concatMap foo . fromList) .
(toList . S.map bar . fromList) .
(toList . S.filter pred . fromList) .
(toList . S.break isSpecial . fromList)

次に、私たちがいつでも全滅させることができることを観察してfromList . toListください:

toList . S.concatMap foo . S.map bar . S.filter pred . S.break . fromList

...そして、チェーンがS.concatMap foo . S.map bar . S.filter pred . S.breakイテレータを内部的に構築してすぐに実際のリストを消滅させることによって暗黙的に構築するのではなく、明示的に構築するため、魔法が起こります。

于 2012-06-08T03:54:15.827 に答える
8

ストリームの利点は、より強力なことです。インターフェース:

data Stream m a = forall s . Stream (s -> m (Step s a)) s Size   

通常のリストではできない多くのことができます。例えば:

  • サイズを追跡します (例: Unknown、Max 34、Exact 12)
  • 次の要素を取得するためにモナド アクションを実行します。リストは部分的に遅延 IO を使用してこれを行うことができますが、その手法はエラーが発生しやすいことが証明されており、通常は初心者または単純な小さなスクリプトでのみ使用されます。

ただし、リストと比較して大きな欠点があります-複雑です! 初心者のプログラマーがストリームを理解するには、存在型とモナド アクションを理解する必要があります。基本的なリスト型を使用するには、これら 2 つの複雑な主題を習得しなければならない場合、haskell を習得するのは非常に困難です。

それを、次のインターフェースを持つリストと比較してください。

data [] a = a : [a] | []

これは非常に単純で、新しいプログラマーに簡単に教えることができます。

リストのもう 1 つの利点は、簡単にパターン マッチできることです。例えば:

getTwo (a : b : _) = Just (a,b)
getTwo _ = Nothing

これは、経験豊富なプログラマ (私は今でも多くの方法でリスト パターン マッチングを使用しています) と、リストの操作に使用できる標準の高階関数をまだ学んでいない初心者プログラマの両方に役立ちます。

ghc はリストの融合に多くの時間を費やしてきたので、効率性もリストのもう 1 つの潜在的な利点です。多くのコードでは、中間リストは生成されません。これは、ストリームで最適化するのがはるかに困難になる可能性があります。

したがって、リストを Streams と交換するのは適切ではないと思います。必要に応じてそれらを持ち込める現在の状況の方が優れていますが、初心者は複雑さにとらわれず、熟練したユーザーはパターン マッチングを失う必要はありません。

編集:約[1..1000000]

これはenumFromTo 1 1000000、遅延評価され、融合の対象となる と同等です (非常に効率的です)。たとえばsum [1..1000000]、最適化がオンになっていると、リストは生成されません (そして定数メモリが使用されます)。したがって、ケース (2) は正しいです。この状況は、遅延評価によるストリームの利点ではありません。ただし、前述のように、ストリームにはリストよりも他の利点があります。

于 2012-06-08T03:33:51.043 に答える
7

簡単な答え: リストとストリームは比類のない力です。ストリームはモナド アクションを許可しますが、共有は禁止しますが、リストはその逆です。

より長い答え:

1) リストでは実装できない反例については @nanothief を参照してください 2) 以下は、ストリームでは簡単に実装できない反例です

問題は、おもちゃのリストの例では通常、リストの共有機能が使用されていないことです。コードは次のとおりです。

foo = map heavyFunction bar
baz = take 5 foo
quux = product foo

リストを使用すると、重い関数を一度だけ計算します。baz余分な計算を行わずにquux ストリームを計算するコードheavyFunctionは、維持するのが難しくなります。

于 2012-06-08T08:18:52.013 に答える