式の 遅延評価 は、プログラマーがコードの実行順序を制御できなくなる可能性があるようです。なぜこれがプログラマーに受け入れられる、または望まれるのか理解できません。
式が評価される時期と場所が保証されていない場合、このパラダイムを使用して、意図したとおりに機能する予測可能なソフトウェアを構築するにはどうすればよいですか?
答えの多くは、無限リストや計算の評価されていない部分からのパフォーマンスの向上などに行きますが、これは遅延の大きな動機が欠けています:modularity。
古典的な議論は、引用数の多い論文 "なぜ関数型プログラミングが重要か"(PDFリンク) ジョンヒューズによって示されています。その論文の主要な例(セクション5)は、アルファベータ検索アルゴリズムを使用してTic-Tac-Toeをプレイしています。重要なポイントは(p。9)です。
[遅延評価]は、プログラムを多数の可能な回答を構成するジェネレーターとして、また適切なものを選択するセレクターとしてモジュール化することを実用的にします。
Tic-Tac-Toeプログラムは、特定の位置から始まるゲームツリー全体を生成する関数と、それを消費する別の関数として作成できます。実行時に、これは本質的にゲームツリー全体を生成するのではなく、消費者が実際に必要とするサブパートのみを生成します。消費者を変更することで、代替案が生成される順序と組み合わせを変更できます。ジェネレータを変更する必要はまったくありません。
熱心な言語では、おそらくツリーを生成するのに多くの時間とメモリを費やしてしまうため、この方法でそれを書くことはできません。つまり、次のいずれかになります。
式が評価される時期と場所が保証されていない場合、このパラダイムを使用して、意図したとおりに機能する予測可能なソフトウェアを構築するにはどうすればよいですか?
式に副作用がない場合、式が評価される順序は値に影響しないため、プログラムの動作は順序によって影響を受けません。したがって、動作は完全に予測可能です。
今、副作用は別の問題です。副作用が任意の順序で発生する可能性がある場合、プログラムの動作は実際に予測できません。しかし、実際にはそうではありません。 Haskellのような怠惰な言語では、参照を透過的にすることがポイントになります。つまり、式が評価される順序が結果に影響を与えないようにします。 Haskellでは、これはユーザーに見える副作用を伴うすべての操作をIOモナド内で発生させることによって強制されます。これにより、すべての副作用が期待どおりの順序で発生することが保証されます。
データベースに精通している場合、データを処理する非常に頻繁な方法は次のとおりです。
select * from foobar
のような質問をする結果の生成方法と方法(インデックス?全表スキャン?)、またはいつ(すべてのデータを一度に生成するか、または要求されたときにインクリメンタルに生成するか)を制御しません。あなたが知っているのは:ifより多くのデータがあり、あなたがそれを要求したときにあなたはそれを得るでしょう。
遅延評価は同じことにかなり近いです。たとえば、無限リストがieとして定義されているとします。フィボナッチ数列-5つの数値が必要な場合は、5つの数値が計算されます。 1000が必要な場合は1000になります。コツは、ランタイムがどこに、いつ提供するかを知っているということです。とても便利です。
(Javaプログラマーは、イテレーターを使用してこの動作をエミュレートできます-他の言語にも同様の機能がある場合があります)
データベースに、名前が「Ab」で始まり20年以上古い最初の2000ユーザーのリストを要求することを検討してください。また、彼らは男性でなければなりません。
これが小さな図です。
You Program Processor
------------------------------------------------------------------------------
Get the first 2000 users ---------->---------- OK!
--------------------- So I'll go get those records...
WAIT! Also, they have to ---------->---------- Gotcha!
start with "Ab"
--------------------- NOW I'll get them...
WAIT! Make sure they're ---------->---------- Good idea Boss!
over 20!
--------------------- Let's go then...
And one more thing! Make ---------->---------- Anything else? Ugh!
sure they're male!
No that is all. :( ---------->---------- FINE! Getting records!
--------------------- Here you go.
Thanks Postgres, you're ---------->---------- ...
my only friend.
この恐ろしい恐ろしい相互作用からわかるように、「データベース」は、すべての条件を処理する準備ができるまで、実際には何も実行していません。各ステップで結果を遅延読み込みし、毎回新しい条件を適用します。
最初の2000人のユーザーを取得するのではなく、それらを返し、 "Ab"をフィルタリングし、それらを返し、20以上をフィルタリングし、それらを返し、男性をフィルタリングし、最後にそれらを返します。
一言で言えば、遅延読み込み。
式の遅延評価により、特定のコードの設計者は、コードが実行されるシーケンスを制御できなくなります。
結果が同じであれば、設計者は式が評価される順序を気にする必要はありません。評価を延期することにより、一部の式を完全に評価することを回避でき、時間を節約できます。
同じアイデアがより低いレベルで機能していることがわかります。多くのマイクロプロセッサは、命令を順不同で実行できるため、さまざまな実行ユニットをより効率的に使用できます。重要なのは、命令間の依存関係を調べ、結果を変更する場所での並べ替えを回避することです。
遅延評価にはいくつか説得力があります。
Modularity遅延評価を使用すると、コードを複数の部分に分割できます。たとえば、「逆数が1未満になるように、リストリスト内の要素の最初の10の逆数を見つける」という問題があるとします。 Haskellのようなもので書くことができます
take 10 . filter (<1) . map (1/)
ただし、厳密な言語ではこれは正しくありません。[2,3,4,5,6,7,8,9,10,11,12,0]
を指定すると、ゼロで除算されるためです。これが実際に素晴らしい理由については、sacundimの回答を参照してください
その他の機能厳密に(しゃれた)厳密な評価よりも厳密でない評価で終了するプログラムが多くなります。プログラムが「熱心な」評価戦略で終了する場合、プログラムは「遅延」評価戦略で終了しますが、反対が真ではありません。この現象の具体的な例として、無限のデータ構造(本当にちょっとだけクールなもの)のようなものが得られます。より多くのプログラムが遅延言語で動作します。
Optimality Call-by-need評価は、時間に関して漸近的に最適です。主要な遅延言語(本質的にはHaskellおよびHaskell)は必要な呼び出しを約束していませんが、最適なコストモデルを期待できます。厳密性アナライザー(および投機的評価)は、実際にはオーバーヘッドを抑えます。スペースはもっと複雑な問題です。
Forces Purity遅延評価を使用すると、副作用を無秩序な方法で処理することが全体の苦痛になります。それは、プログラマーがコントロールを失うためです。これは良いことです。参照の透明性により、プログラムのプログラミング、屈折、および推論が非常に簡単になります。厳密な言語は、不純なビットを持つというプレッシャーに必然的に陥ります。HaskellやCleanが美しく抵抗してきたものがあります。これは副作用が常に悪だと言っているわけではありませんが、それらを制御することは非常に有用であり、この理由だけで遅延言語を使用するのに十分です。
高価な計算がたくさんあるものの、実際にどの計算が必要か、またはその順序がわからないとします。複雑なmother-may-iプロトコルを追加して、消費者に何が利用可能かを理解させ、まだ行われていない計算をトリガーさせることができます。または、計算がすべて完了したかのように動作するインターフェースを提供することもできます。
また、無限の結果があるとします。たとえば、すべての素数のセット。セットを事前に計算できないことは明らかなので、素数の領域での演算はすべて遅延する必要があります。
遅延評価を使用すると、コード実行に関する制御を失うことはありませんが、それでも完全に確定的です。でも慣れるのは難しいです。
遅延評価は、積極的な評価が失敗するがその逆ではない場合に終了するラムダ項を削減する方法であるため、便利です。これには、1)実際に計算を実行する前に計算結果にリンクする必要がある場合(たとえば、循環グラフ構造を構築するが、関数スタイルで実行したい場合)2)無限データ構造を定義するが、この構造フィードを機能させる場合データ構造の一部のみを使用する。