なぜ遅延評価が便利なのかとずっと思っていました。私はまだ誰かに意味のある方法で説明してもらう必要があります。主に「私を信頼する」ために煮詰めてしまう。
注:私はメモを意味するものではありません。
なぜ遅延評価が便利なのかとずっと思っていました。私はまだ誰かに意味のある方法で説明してもらう必要があります。主に「私を信頼する」ために煮詰めてしまう。
注:私はメモを意味するものではありません。
回答:
主にそれがより効率的である可能性があるため-値が使用されない場合、値を計算する必要はありません。たとえば、3つの値を関数に渡すことができますが、条件式のシーケンスによっては、実際にはサブセットのみが使用される場合があります。Cのような言語では、3つの値はすべてとにかく計算されます。しかしHaskellでは、必要な値のみが計算されます。
それはまた、無限リストのようなクールなものを可能にします。Cのような言語では無限のリストを作成することはできませんが、Haskellでは問題ありません。無限リストは、数学の特定の領域でかなり頻繁に使用されるため、それらを操作する機能があると便利です。
遅延評価の有用な例は、次の使用ですquickSort
。
quickSort [] = []
quickSort (x:xs) = quickSort (filter (< x) xs) ++ [x] ++ quickSort (filter (>= x) xs)
リストの最小値を見つけたい場合は、次のように定義できます。
minimum ls = head (quickSort ls)
どちらが最初にリストをソートし、次にリストの最初の要素を取得します。ただし、遅延評価のため、頭だけが計算されます。たとえば、リストの最小値を取る場合、[2, 1, 3,]
quickSortは最初に2より小さいすべての要素をフィルターで除外します。次に、それでQuickSortを実行し(シングルトンリスト[1]を返します)、すでに十分です。遅延評価のため、残りはソートされないため、計算時間を大幅に節約できます。
これはもちろん非常に単純な例ですが、遅延は非常に大きなプログラムでも同じように機能します。
ただし、これらすべての欠点があります。プログラムのランタイム速度とメモリ使用量を予測することが難しくなります。これは、遅延プログラムが遅くなる、またはより多くのメモリを使用することを意味するものではありませんが、知っておくとよいでしょう。
take k $ quicksort list
O(n + k log k)時間しかかかりませんn = length list
。非遅延比較ソートでは、これは常にO(n log n)時間かかります。
遅延評価は多くのことに役立ちます。
まず、既存の遅延言語はすべて純粋です。遅延言語の副作用について考えるのは非常に難しいためです。
純粋な言語では、方程式の推論を使用して関数の定義を推論できます。
foo x = x + 3
残念ながら、遅延のない設定では、遅延の設定よりも多くのステートメントが返されないため、これはMLなどの言語ではあまり役に立ちません。しかし、怠惰な言語では、平等について安全に推論できます。
次に、MLの「値の制限」のような多くのことはHaskellのような怠惰な言語では必要ありません。これにより、構文がすっきりと整理されます。MLのような言語では、varやfunなどのキーワードを使用する必要があります。Haskellでは、これらは1つの概念に集約されます。
第3に、遅延は、部分的に理解できる非常に機能的なコードを記述できるようにします。Haskellでは、次のような関数本体を書くのが一般的です。
foo x y = if condition1
then some (complicated set of combinators) (involving bigscaryexpression)
else if condition2
then bigscaryexpression
else Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
これにより、関数の本体を理解しながら「トップダウン」で作業できます。MLのような言語では、let
では、厳密に評価さ。したがって、高価な(または副作用がある)場合は常に評価されたくないので、関数の本体にlet句をあえて「持ち上げる」ことはしません。Haskellは、その句の内容が必要な場合にのみ評価されることを知っているため、詳細をwhere句に明示的にプッシュできます。
実際には、ガードを使用し、さらに次の目的で折りたたむ傾向があります。
foo x y
| condition1 = some (complicated set of combinators) (involving bigscaryexpression)
| condition2 = bigscaryexpression
| otherwise = Nothing
where some x y = ...
bigscaryexpression = ...
condition1 = ...
condition2 = ...
第4に、遅延は特定のアルゴリズムをよりエレガントに表現できる場合があります。Haskellのレイジーな「クイックソート」はワンライナーであり、最初の数項目のみを見ると、それらの項目のみを選択するコストに比例したコストしか支払わないという利点があります。これを厳密に行うことを妨げるものは何もありませんが、同じ漸近的なパフォーマンスを実現するには、毎回アルゴリズムを再コード化する必要があります。
第5に、遅延により、言語で新しい制御構造を定義できます。厳密な言語で新しい「if .. then .. else ..」のような構成を書くことはできません。次のような関数を定義しようとすると、
if' True x y = x
if' False x y = y
厳密な言語では、条件値に関係なく両方のブランチが評価されます。ループを考えるとさらに悪化します。すべての厳密なソリューションでは、何らかの引用または明示的なラムダ構造を提供する言語が必要です。
最後に、同じ流れで、モナドなど、型システムでの副作用を処理するための最良のメカニズムのいくつかは、実際には遅延設定でのみ効果的に表現できます。これは、F#のワークフローの複雑さとHaskell Monadsを比較することで確認できます。(厳密な言語でモナドを定義することはできますが、残念なことに、怠惰とワークフローの不足により、1つまたは2つのモナドの法則が失敗することが多く、厳密な手荷物を大量に受け取ります。)
let
は、再帰は危険な獣です。R6RSスキームで#f
は、結び目を結ぶことがサイクルにつながるどこにでも、ランダムに単語を出現させます!しゃれは意図されていませんlet
が、遅延言語では厳密にもっと再帰的なバインディングが賢明です。where
厳密さは、SCCを除いて、相対的な効果を順序付ける方法がないという事実をさらに悪化させます。これはステートメントレベルの構造であり、その効果は厳密に任意の順序で発生する可能性があり、純粋な言語であっても、#f
問題。厳格なwhere
非ローカルの懸念でコードをなぞります。
ifFunc(True, x, y)
両方x
ではy
なく単に評価するためですx
。
通常の順序の評価と遅延評価(Haskellのように)の間には違いがあります。
square x = x * x
次の式を評価しています...
square (square (square 2))
...熱心な評価付き:
> square (square (2 * 2))
> square (square 4)
> square (4 * 4)
> square 16
> 16 * 16
> 256
...通常の注文評価で:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * (square (square 2))
> ((2 * 2) * (square 2)) * (square (square 2))
> (4 * (square 2)) * (square (square 2))
> (4 * (2 * 2)) * (square (square 2))
> (4 * 4) * (square (square 2))
> 16 * (square (square 2))
> ...
> 256
...遅延評価あり:
> (square (square 2)) * (square (square 2))
> ((square 2) * (square 2)) * ((square 2) * (square 2))
> ((2 * 2) * (2 * 2)) * ((2 * 2) * (2 * 2))
> (4 * 4) * (4 * 4)
> 16 * 16
> 256
これは、遅延評価が構文ツリーを調べてツリー変換を行うためです...
square (square (square 2))
||
\/
*
/ \
\ /
square (square 2)
||
\/
*
/ \
\ /
*
/ \
\ /
square 2
||
\/
*
/ \
\ /
*
/ \
\ /
*
/ \
\ /
2
...通常の順序評価はテキスト展開のみを行います。
そのため、遅延評価を使用すると、パフォーマンスは(少なくともO表記では)熱心な評価と同等でありながら、より強力になります(評価は他の戦略よりも頻繁に終了します)。
CPUに関連する遅延評価は、RAMに関連するガベージコレクションと同じです。GCを使用すると、メモリが無制限にあるように見せかけて、メモリ内のオブジェクトを必要なだけ要求できます。ランタイムは、使用できないオブジェクトを自動的に回収します。LEを使用すると、計算リソースが無制限であるかのように見せかけることができます。必要なだけ計算を実行できます。ランタイムは不必要な(与えられたケースの)計算を実行しません。
これらの「ふり」モデルの実際的な利点は何ですか?開発者を(ある程度)リソース管理から解放し、ソースから定型コードを削除します。しかし、より重要なことは、幅広いコンテキストでソリューションを効率的に再利用できることです。
数字Sと数字Nのリストがあると想像してください。リストSから数字Nに最も近い数字Mを見つける必要があります。2つのコンテキストを持つことができます:シングルNとNのいくつかのリストL(Lの各Nのei) Sで最も近いMを検索します)。遅延評価を使用する場合、Sを並べ替えてバイナリ検索を適用し、Nに最も近いMを見つけることができます。適切な遅延並べ替えを行うには、単一のNに対してO(size(S))ステップとO(ln(size(S))*が必要です。 (size(S)+ size(L)))均等に分散されたLのステップ。最適な効率を達成するための遅延評価がない場合は、各コンテキストにアルゴリズムを実装する必要があります。
サイモンペイトンジョーンズを信じている場合、怠惰な評価はそれ自体重要ではなく、デザイナーに言語を純粋に保つことを強いた「シャツ」としてのみ重要です。私はこの観点に共感しています。
リチャード・バード、ジョン・ヒューズ、そして多少なりとも、ラルフ・ヒンズは遅延評価で驚くべきことを行うことができます。彼らの作品を読むことはあなたがそれを理解するのに役立ちます。良い出発点は Birdの素晴らしい数独ソルバーとHughesの「関数型プログラミングが重要である理由」に関する論文があります。
IO
main
String -> String
IO
モナドに強制するのを止めているのは何ですか?
三目並べプログラムを考えてみましょう。これには4つの機能があります。
これにより、懸念が明確に分離されます。特に、移動生成関数とボード評価関数は、ゲームのルールを理解する必要がある唯一の関数です。移動ツリーとミニマックス関数は完全に再利用可能です。
次に、tic-tac-toeの代わりにチェスを実装してみましょう。「熱心な」(つまり従来の)言語では、移動ツリーがメモリに収まらないため、これは機能しません。したがって、ボードの評価および移動生成関数は、移動する移動を決定するためにミニマックスロジックを使用する必要があるため、移動ツリーおよびミニマックスロジックと混合する必要があります。きれいなモジュール構造がなくなります。
ただし、遅延言語では、移動ツリーの要素はminimax関数からの要求に応答してのみ生成されます。最上位の要素でminimaxを解放する前に、移動ツリー全体を生成する必要はありません。したがって、クリーンなモジュール構造は実際のゲームでも機能します。
ここでは、まだ議論で取り上げられていないと思われる2つのポイントを示します。
怠惰は、並行環境における同期メカニズムです。これは、いくつかの計算への参照を作成し、その結果を多くのスレッド間で共有するための軽量で簡単な方法です。複数のスレッドが未評価の値にアクセスしようとすると、そのうちの1つだけがそれを実行し、他のスレッドはそれに応じてブロックし、値が利用可能になると値を受け取ります。
怠惰は、純粋な設定でデータ構造を償却するための基本です。これは岡崎によって純粋に機能的なデータ構造で詳細に説明されていますが、基本的な考え方は、遅延評価は特定のタイプのデータ構造を効率的に実装するために重要な突然変異の制御された形式であるということです。私たちはしばしば、私たちが純粋なヘアシャツを着用することを強いる怠惰について話しますが、逆の方法も適用されます。それらは、相乗的な言語機能のペアです。
コンピューターの電源を入れ、Windowsがエクスプローラーでハードドライブのすべての単一のディレクトリを開かないようにし、コンピューターにインストールされているすべての単一のプログラムを起動しないようにすると、特定のディレクトリが必要であるか、特定のプログラムが必要であることが示されるまで、 「遅延」評価です。
「遅延」評価とは、必要なときに必要なときに操作を実行することです。これは、プログラミング言語またはライブラリの機能である場合に役立ちます。通常、事前にすべてを事前に計算するよりも、遅延評価を自分で実装することが難しいからです。
このことを考慮:
if (conditionOne && conditionTwo) {
doSomething();
}
メソッドdoSomething()は、conditionOneがtrueで、 conditionTwoがtrueの場合にのみ実行されます。conditionOneがfalseの場合、なぜconditionTwoの結果を計算する必要があるのですか?この場合、特に条件が何らかのメソッドプロセスの結果である場合、conditionTwoの評価は時間の無駄になります。
これは遅延評価の興味の一例です...
効率を高めることができます。これは明白に見えるものですが、実際には最も重要ではありません。(注も怠惰ができることを殺しすぎて効率を-この事実はすぐに明らかにされていませんが、一時的な結果の多くを保存するのではなく、すぐにそれらを計算することで、あなたはRAMの膨大な量を使用することができます。)
言語にハードコードするのではなく、通常のユーザーレベルのコードでフロー制御構造を定義できます。(たとえば、Javaにはfor
ループがあります。Haskellにはfor
関数があります。Javaには例外処理があります。Haskellにはさまざまなタイプの例外モナドがあります。C#にはgoto
; Haskellには継続モナドがあります...)
生成するデータ量を決定するアルゴリズムから、データを生成するアルゴリズムを分離できます。結果の概念的に無限のリストを生成する1つの関数と、このリストを必要と判断しただけ処理する別の関数を作成できます。さらに言えば、5つのジェネレーター関数と5つのコンシューマー関数を使用でき、両方のアクションを一度に組み合わせる5 x 5 = 25関数を手動でコーディングする代わりに、任意の組み合わせを効率的に生成できます。(!)デカップリングが良いことであることは誰もが知っています。
それは多かれ少なかれ純粋な関数型言語を設計することを強制します。常にショートカットを取るのは魅力的ですが、怠惰な言語では、わずかな不純物によってコードが非常に予測不可能になり、ショートカットを取るのを強く抑えます。
遅延の1つの大きな利点は、合理的な償却境界を持つ不変のデータ構造を作成できることです。簡単な例は、不変スタック(F#を使用)です。
type 'a stack =
| EmptyStack
| StackNode of 'a * 'a stack
let rec append x y =
match x with
| EmptyStack -> y
| StackNode(hd, tl) -> StackNode(hd, append tl y)
コードは妥当ですが、2つのスタックxとyを追加すると、最良、最悪、および平均の場合にO(xの長さ)の時間がかかります。2つのスタックの追加はモノリシック操作であり、スタックxのすべてのノードに接触します。
データ構造を遅延スタックとして書き直すことができます。
type 'a lazyStack =
| StackNode of Lazy<'a * 'a lazyStack>
| EmptyStack
let rec append x y =
match x with
| StackNode(item) -> Node(lazy(let hd, tl = item.Force(); hd, append tl y))
| Empty -> y
lazy
コンストラクタでコードの評価を一時停止することで機能します。を使用して評価されると.Force()
と、戻り値はキャッシュされ、後続のすべてのときに再利用されます.Force()
。
遅延バージョンでは、追加はO(1)操作です。1つのノードを返し、リストの実際の再構築を一時停止します。このリストの先頭を取得すると、ノードのコンテンツを評価し、強制的に先頭を返し、残りの要素で1つのサスペンションを作成するため、リストの先頭を取得することはO(1)操作です。
したがって、私たちの遅延リストは常に再構築の状態にあり、すべての要素を走査するまで、このリストを再構築するためのコストを支払う必要はありません。遅延を使用して、このリストはO(1)のconsingおよびappendingをサポートします。興味深いことに、アクセスされるまでノードを評価しないため、潜在的に無限の要素を持つリストを構築することは完全に可能です。
上記のデータ構造では、各トラバーサルでノードを再計算する必要がないため、.NETのバニラIEnumerablesとは明らかに異なります。
このスニペットは、遅延評価と遅延評価の違いを示しています。もちろん、このフィボナッチ関数自体を最適化して、再帰の代わりに遅延評価を使用することもできますが、これでは例が台無しになります。
我々は仮定しようMAYだけで、必要に応じて遅延評価とそれらが生成されます、すべての20個の数字が先行生成されている必要はありませ遅延評価で、何かのために20の最初の番号を使用する必要がありますが、。したがって、必要なときに計算価格のみを支払うことになります。
出力例
遅延生成ではない:0.023373 遅延生成:0.000009 遅延しない出力:0.000921 遅延出力:0.024205
import time
def now(): return time.time()
def fibonacci(n): #Recursion for fibonacci (not-lazy)
if n < 2:
return n
else:
return fibonacci(n-1)+fibonacci(n-2)
before1 = now()
notlazy = [fibonacci(x) for x in range(20)]
after1 = now()
before2 = now()
lazy = (fibonacci(x) for x in range(20))
after2 = now()
before3 = now()
for i in notlazy:
print i
after3 = now()
before4 = now()
for i in lazy:
print i
after4 = now()
print "Not lazy generation: %f" % (after1-before1)
print "Lazy generation: %f" % (after2-before2)
print "Not lazy output: %f" % (after3-before3)
print "Lazy output: %f" % (after4-before4)
遅延評価は、データ構造で最も役立ちます。構造内の特定のポイントのみを誘導的に指定し、配列全体に関して他のすべてを表現する配列またはベクトルを定義できます。これにより、非常に簡潔かつ高い実行時パフォーマンスでデータ構造を生成できます。
この動作を確認するには、本能と呼ばれる私のニューラルネットワークライブラリをご覧ください。優雅さと高性能のために遅延評価を多用します。たとえば、私は伝統的に命令型アクティベーション計算を完全に取り除きます。単純な怠惰な表現は私のためにすべてを行います。
これは、たとえばアクティベーション関数やバックプロパゲーション学習アルゴリズムで使用されます(私は2つのリンクしか投稿できないのでlearnPat
、AI.Instinct.Train.Delta
自分でモジュールで関数を検索する必要があります)。従来はどちらも、はるかに複雑な反復アルゴリズムが必要です。
他の人々はすでに大きな理由をすべて述べましたが、怠惰が重要である理由を理解するのに役立つ有用な演習は、厳密な言語で固定小数点関数を書いてみることです。
Haskellでは、固定小数点関数は非常に簡単です。
fix f = f (fix f)
これは拡大する
f (f (f ....
しかし、Haskellは怠惰なので、その無限の計算チェーンは問題ありません。評価は「外側から内側へ」行われ、すべてが見事に機能します。
fact = fix $ \f n -> if n == 0 then 1 else n * f (n-1)
重要なのは、それfix
が怠惰であることではなく、怠惰であるf
ことです。いったんstrictを与えられf
たら、手を空中に投げてあきらめるか、etaを使って展開して、物を整理することができます。(これは、言語ではなく、厳格で怠惰なライブラリであるというノアの発言とよく似ています)。
厳密なScalaで同じ関数を書くことを想像してください:
def fix[A](f: A => A): A = f(fix(f))
val fact = fix[Int=>Int] { f => n =>
if (n == 0) 1
else n*f(n-1)
}
もちろん、スタックオーバーフローが発生します。機能させるには、必要に応じてf
引数を呼び出す必要があります。
def fix[A](f: (=>A) => A): A = f(fix(f))
def fact1(f: =>Int=>Int) = (n: Int) =>
if (n == 0) 1
else n*f(n-1)
val fact = fix(fact1)
あなたが現在どのように考えているのかはわかりませんが、遅延評価を言語機能ではなくライブラリの問題と考えると便利だと思います。
つまり、厳密な言語では、いくつかのデータ構造を構築することで遅延評価を実装でき、遅延言語(少なくともHaskell)では、必要なときに厳密さを要求できます。したがって、言語の選択によってプログラムが遅延することも遅延しないこともありませんが、デフォルトで取得するプログラムに影響を与えるだけです。
そのように考えたら、後でデータを生成するために使用できるデータ構造を作成するすべての場所を考えてください(その前にあまり詳しく調べることなく)。遅延の多くの使用法がわかります。評価。
私が使用した遅延評価の最も有用な活用は、特定の順序で一連のサブ関数を呼び出す関数でした。これらのサブ関数のいずれかが失敗した(falseが返された)場合、呼び出し元の関数はすぐに戻る必要がありました。だから私はこのようにそれをすることができたでしょう:
bool Function(void) {
if (!SubFunction1())
return false;
if (!SubFunction2())
return false;
if (!SubFunction3())
return false;
(etc)
return true;
}
または、よりエレガントなソリューション:
bool Function(void) {
if (!SubFunction1() || !SubFunction2() || !SubFunction3() || (etc) )
return false;
return true;
}
使い始めると、ますます頻繁に使用する機会が見えてきます。
遅延評価は貧弱な人の方程式の推論です(これは、理想的には、関係する型のプロパティと操作のコードからコードのプロパティを推定することであると期待できます)。
うまく機能する例:sum . take 10 $ [1..10000000000]
。直接で単純な数値計算を1つだけ行うのではなく、合計を10の数に減らしてもかまいません。もちろん、遅延評価がなければ、最初の10個の要素を使用するだけで、メモリ内に巨大なリストが作成されます。確かに非常に遅くなり、メモリ不足エラーが発生する可能性があります。
私たちが好きな、それは素晴らしいようではありません。例:sum . take 1000000 . drop 500 $ cycle [1..20]
。これは、リスト内ではなくループ内であっても、実際には1 000 000の数値を合計します。それでも、条件と式がほとんどない、直接の数値計算を1つに減らす必要があります。これは、1 000 000の数値を合計するよりもはるかに優れています。ループ内であっても、リスト内ではない場合(つまり、森林破壊最適化後)。
もう一つは、それは、コードすることを可能にする末尾再帰法と短所のスタイル、そしてそれはちょうど働きます。
cf. 関連回答。
「遅延評価」とは、次のような結合ブール値のようなものを意味します
if (ConditionA && ConditionB) ...
その答えは、プログラムが消費するCPUサイクルが少ないほど、実行が速くなるということです。処理命令のチャンクがプログラムの結果に影響を与えない場合、それは不要です(したがって、無駄になります)。とにかく)それらを実行するには...
もしそうなら、あなたは私が「レイジーイニシャライザ」として知っていることを意味します、例えば:
class Employee
{
private int supervisorId;
private Employee supervisor;
public Employee(int employeeId)
{
// code to call database and fetch employee record, and
// populate all private data fields, EXCEPT supervisor
}
public Employee Supervisor
{
get
{
return supervisor?? (supervisor = new Employee(supervisorId));
}
}
}
まあ、この手法により、クラスを使用するクライアントコードは、従業員オブジェクトを使用するクライアントがスーパーバイザーのデータへのアクセスを必要とする場合を除いて、スーパーバイザーデータレコードのデータベースを呼び出す必要を回避できます...これにより、従業員のインスタンス化のプロセスが速くなり、さらに、スーパーバイザーが必要な場合は、スーパーバイザープロパティへの最初の呼び出しでデータベース呼び出しがトリガーされ、データがフェッチされて使用可能になります...
高次関数からの抜粋
3829で割り切れる100,000未満の最大数を見つけましょう。これを行うには、解が存在することがわかっている可能性のセットをフィルター処理するだけです。
largestDivisible :: (Integral a) => a
largestDivisible = head (filter p [100000,99999..])
where p x = x `mod` 3829 == 0
最初に、100,000未満のすべての数値のリストを降順で作成します。次に、それを述語でフィルター処理します。数値は降順で並べ替えられるため、述語を満たす最大の数は、フィルター処理されたリストの最初の要素です。開始セットに有限のリストを使用する必要さえありませんでした。それは再び怠惰です。フィルターされたリストの先頭のみを使用することになるため、フィルターされたリストが有限であるか無限であるかは問題ではありません。最初の適切な解が見つかると、評価は停止します。