遅延評価がどこでも使用されないのはなぜですか?


32

怠justな評価がどのように機能するかを学んだところ、私は疑問に思っていました:なぜ怠producedな評価が現在生産されているすべてのソフトウェアに適用されないのですか?まだ熱心な評価を使用しているのはなぜですか?


2
可変状態と遅延評価を混在させた場合に発生する可能性のある例を次に示します。alicebobandmallory.com/articles/2011/01/01/...
ジョナスElfström

2
@JonasElfström:可変状態とその実装の1つを混同しないでください。可変状態は、値の無限の遅延ストリームを使用して実装できます。そうすれば、可変変数の問題はありません。
ジョルジオ

命令型プログラミング言語では、「遅延評価」にはプログラマーの意識的な努力が必要です。命令型言語での汎用プログラミングはこれを簡単にしましたが、決して透明になることはありません。質問の反対側の答えは、「なぜ関数型プログラミング言語はどこでも使用されないのか」という別の質問を導き出し、現在の答えは時事問題として単に「いいえ」です。
ルワン

2
関数型プログラミング言語は、ネジにハンマーを使用しない同じ理由でどこでも使用されていません。すべての問題を機能的な入力->出力方法で簡単に表現できるわけではありません。たとえば、GUIは命令的な方法で表現するのに適しています。
ALXGTV

さらに、2つのクラスの関数型プログラミング言語(または少なくとも両方が関数型であると主張)、命令型関数型言語(Clojure、Scalaなど)と宣言型(Haskell、OCamlなど)があります。
ALXGTV

回答:


38

遅延評価では、簿記のオーバーヘッドが必要です。評価済みかどうかなどを知る必要があります。熱心な評価は常に評価されるので、知る必要はありません。これは特に同時コンテキストで当てはまります。

次に、必要に応じて、後で呼び出すために関数オブジェクトにパッケージ化することにより、熱心な評価を遅延評価に変換するのは簡単です。

第三に、遅延評価は制御の喪失を意味します。ディスクからファイルの読み取りを遅延評価した場合はどうなりますか?または時間を取得しますか?それは受け入れられません。

熱心な評価はより効率的で制御しやすく、簡単に遅延評価に変換されます。遅延評価が必要なのはなぜですか?


10
ディスクからのファイルの遅延読み取りは実際には本当にすてきです。私の単純なプログラムとスクリプトのほとんどにとって、Haskell readFileまさに必要なものです。また、遅延評価から熱心な評価への変換も簡単です。
ティコンジェルビス

3
最後の段落を除くすべてに同意します。チェーン操作があった場合に怠惰な評価は、より効率的であり、そしてそれはあなたが実際にデータを必要とするときのより多くの制御を持つことができます
texasbruce

4
ファンクタの法則は、「制御の喪失」に関してあなたに一言伝えたいと思っています。不変のデータ型を操作する純粋な関数を作成する場合、遅延評価は天の恵みです。haskellのような言語は、基本的に怠lazの概念に基づいています。一部の言語では、特に「安全でない」コードと混ざっている場合は扱いにくいですが、怠lazはデフォルトで危険または悪いように聞こえます。危険なコードでは「危険」なだけです。
サラ

1
@DeadMGあなたのコードが終了するかどうか気にしないなら... head [1 ..]Haskellではそれが与えるので、熱心に評価された純粋な言語であなたに何を与え1ますか?
セミコロン

1
多くの言語では、遅延評価を実装すると、少なくとも複雑さが生じます。場合によってはその複雑さが必要になり、遅延評価を行うと全体的な効率が向上します。特に、評価対象が条件付きでのみ必要な場合はそうです。ただし、不完全に実行すると、微妙なバグが発生したり、コードを記述する際の誤った仮定によるパフォーマンスの問題を説明するのが難しくなったりします。トレードオフがあります。
ベリンロリチュ

17

怠zyなコードと状態がうまく混ざり合い、バグを見つけるのが難しいためです。依存オブジェクトの状態が変化すると、評価時に遅延オブジェクトの値が間違っている可能性があります。状況が適切だとわかったときにプログラマーがオブジェクトを怠laになるように明示的にコーディングする方がはるかに優れています。

サイドノートでは、Haskellはすべてに対して遅延評価を使用します。これは関数型言語であり、状態を使用しないため可能です(明確にマークされているいくつかの例外的な状況を除く)


ええ、可変状態+怠evaluationな評価=死。SICP決勝で失った唯一のポイントset!は、怠SchemeなSchemeインタープリターでの使用についてだったと思います。> :(
Tikhon Jelvis

3
「怠lazなコードと状態はひどく混在する可能性があります」:状態の実装方法によって異なります。共有の可変変数を使用して実装し、状態の一貫性を評価する順序に依存している場合、あなたは正しいです。
ジョルジオ

14

遅延評価は常に良いとは限りません。

遅延評価のパフォーマンス上の利点は大きい可能性がありますが、熱心な環境でほとんどの不必要な評価を避けるのは難しくありません-遅延は確かに簡単で完全になりますが、コードで不必要な評価が大きな問題になることはめったにありません。

遅延評価の良いところは、より明確なコードを書くことができるときです。無限の自然数リストをフィルタリングし、そのリストの10番目の要素を取得して10番目の素数を取得することは、最も簡潔で明確な処理方法の1つです(疑似コード)。

let numbers = [1,2...]
fun is_prime x = none (map (y-> x mod y == 0) [2..x-1])
let primes = filter is_prime numbers
let tenth_prime = first (take primes 10)

怠zyなくして簡潔に表現するのは非常に難しいと思います。

しかし、怠zyはすべてに対する答えではありません。手始めに、状態の存在下でレイジーネスを透過的に適用することはできません。また、ステートフルネスを自動的に検出することはできません(たとえば、Haskellで作業している場合を除き、状態は非常に明確です)。そのため、ほとんどの言語では、遅延を手動で行う必要があります。これにより、物事がわかりにくくなり、遅延評価の大きな利点の1つがなくなります。

さらに、遅延評価には、評価されていない式を保持するという大きなオーバーヘッドが発生するため、パフォーマンス上の欠点があります。ストレージを使い果たし、単純な値よりも動作が遅くなります。怠zyなバージョンは犬のように遅いため、コードを熱心に確認する必要があることを知ることは珍しくありません。

それが起こる傾向があるので、絶対的な最高の戦略はありません。Lazyは、無限のデータ構造または他の使用可能な戦略を利用してより良いコードを記述できれば素晴らしいですが、熱心に最適化する方が簡単です。


本当に賢いコンパイラーがオーバーヘッドを大幅に軽減することは可能でしょうか?またはさらに最適化のために怠lazを利用しますか?
ティコンジェルビス

3

以下は、熱心な評価と怠evaluationな評価の長所と短所の簡単な比較です。

  • 熱心な評価:

    • ものを不必要に評価する潜在的なオーバーヘッド。

    • 妨げられない、迅速な評価。

  • 遅延評価:

    • 不要な評価はありません。

    • 値を使用するたびの簿記のオーバーヘッド。

したがって、評価する必要のない式が多数ある場合は、レイジーの方が優れています。それでも、評価する必要のない式がない場合、遅延は純粋なオーバーヘッドです。

それでは、実際のソフトウェアを見てみましょう。あなたが書いた関数のうち、すべての引数の評価を必要としない関数はいくつありますか?特に、1つのことだけを行う最新の短い関数では、このカテゴリに分類される関数の割合は非常に低くなっています。したがって、遅延評価では、ほとんどの場合、簿記のオーバーヘッドが発生するだけで、実際に何かを保存する機会はありません。

その結果、遅延評価は単に平均的には支払われません。熱心な評価は最新のコードにより適しています。


1
「値を使用するたびの簿記のオーバーヘッド。」:簿記のオーバーヘッドは、たとえばJavaのような言語でnull参照をチェックするよりも大きいとは思わない。どちらの場合も、1ビットの情報(評価済み/保留中対null /非null)を確認する必要があり、値を使用するたびに確認する必要があります。そのため、はい、オーバーヘッドがありますが、最小限です。
ジョルジオ

1
「作成する関数のうち、引数のすべてを評価する必要のない関数はいくつありますか?」:これは単なる例のアプリケーションです。再帰的な無限のデータ構造はどうですか?熱心な評価で実装できますか?イテレータを使用できますが、ソリューションは必ずしも簡潔ではありません。もちろん、広範囲に使用する機会が一度もなかったものを見逃さないでしょう。
ジョルジオ

2
「その結果、怠evaluationな評価は単に平均で支払われません。熱心な評価は現代のコードに適しています。」:この文は成り立ちません。実際に実装しようとしているものに依存します。
ジョルジオ

1
@Giorgioオーバーヘッドはさほど大きくないように思えるかもしれませんが、条件は現代のCPUが取るに足らないものの1つです。内側のループに不必要な条件は必要ありません。関数の引数ごとに10サイクル余分に支払うことは、Javaでコードをコーディングするのと同じくらいパフォーマンスに敏感なコードでは受け入れられません。怠evaluationな評価では、熱心な評価では簡単にできないトリックを実行できることは間違いありません。しかし、コードの大部分はこれらのトリックを必要としません。
cmaster

2
これは、遅延評価のある言語の経験不足からの回答のようです。たとえば、無限のデータ構造はどうですか?
アンドレスF.

3

@DeadMGが指摘したように、遅延評価にはブックキーピングのオーバーヘッドが必要です。これは、熱心な評価に比べて費用がかかる場合があります。次のステートメントを検討してください。

i = (243 * 414 + 6562 / 435.0 ) ^ 0.5 ** 3

これには、計算に少し時間がかかります。遅延評価を使用する場合は、使用するたびに評価されているかどうかを確認する必要があります。これが頻繁に使用されるタイトループ内にある場合、オーバーヘッドは大幅に増加しますが、メリットはありません。

熱心な評価と適切なコンパイラを使用すると、式はコンパイル時に計算されます。ほとんどのオプティマイザーは、必要に応じて、発生するループから割り当てを移動します。

遅延評価は、アクセス頻度が低く、取得するオーバーヘッドが大きいデータの読み込みに最適です。したがって、コア機能よりもエッジケースに適しています。

一般的に、できるだけ頻繁にアクセスされるものを評価することをお勧めします。遅延評価はこの方法では機能しません。常に何かにアクセスする場合、遅延評価はオーバーヘッドを追加するだけです。遅延評価を使用するコスト/利点は、アクセスされるアイテムがアクセスされる可能性が低くなるにつれて減少します。

常に遅延評価を使用することは、早期の最適化も意味します。これは悪い慣行であり、コードが非常に複雑で高価になります。残念ながら、時期尚早の最適化により、コードの実行が単純なコードよりも遅くなることがよくあります。最適化の効果を測定できるようになるまで、コードを最適化することはお勧めできません。

時期尚早な最適化を回避することは、優れたコーディング慣行と競合しません。適切なプラクティスが適用されなかった場合、最初の最適化は、計算をループ外に移動するなどの適切なコーディングプラクティスを適用することで構成されます。


1
あなたは経験不足から議論しているようです。Wadlerの論文「Why Functional Programming Matters」を読むことをお勧めします。遅延評価の理由を説明する主要なセクションを提供します(ヒント:パフォーマンス、初期の最適化、または「アクセス頻度の低いデータの読み込み」、およびモジュール性に関するすべてとはほとんど関係ありません)。
アンドレスF.

@AndresFあなたが参照した論文を読みました。そのような場合に遅延評価を使用することに同意します。早期評価は適切ではないかもしれませんが、追加の移動を簡単に追加できる場合、選択した移動のサブツリーを返すことは大きなメリットがあると主張します。ただし、その機能を構築すると、時期尚早な最適化になる可能性があります。関数型プログラミング以外では、レイジー評価の使用に関する重大な問題と、レイジー評価の使用に失敗するという重大な問題があります。関数型プログラミングの遅延評価に起因する大幅なパフォーマンスコストの報告があります。
BillThor

2
といった?熱心な評価を使用する場合にも、著しいパフォーマンスコストの報告があります(不要な評価とプログラムの終了以外の形式のコスト)。他の(誤った)使用される機能にはほとんどコストがかかります。モジュール性自体にコストがかかる場合があります。問題は、それが価値があるかどうかです。
アンドレスF.

3

式の値を決定するために式を完全に評価する必要がある場合、遅延評価は不利になる可能性があります。ブール値の長いリストがあり、それらのすべてが真であるかどうかを調べたいとします。

[True, True, True, ... False]

これを行うには、リスト内のすべての要素を調べる必要があります。そのため、評価を遅らせる可能性はありません。フォールドを使用して、リスト内のすべてのブール値がtrueであるかどうかを判断できます。遅延評価を使用するフォールドライトを使用する場合、リスト内のすべての要素を確認する必要があるため、遅延評価の利点は得られません。

foldr (&&) True [True, True, True, ... False] 
> 0.27 secs

この場合、右折は厳密な左折よりもはるかに遅くなり、遅延評価を使用しません。

foldl' (&&) True [True, True, True, ... False] 
> 0.09 secs

その理由は、厳密なフォールドが末尾再帰を使用するためです。これは、戻り値を蓄積し、大量の操作チェーンを構築してメモリに格納しないことを意味します。どちらの関数もリスト全体をとにかく見る必要があり、フォールドライトは末尾再帰を使用できないため、これはレイジーフォールドライトよりもはるかに高速です。だから、ポイントは、手元のタスクに最適なものを使用する必要があるということです。


「だから、ポイントは、手元のタスクに最適なものを使用する必要があるということです。」+1
ジョルジオ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.