私は、なぜ遅延評価が役立つのかと長い間考えていました。意味のある方法で誰かに説明してもらうことはまだありません。ほとんどが「私を信頼する」ために沸騰します。
注:メモ化を意味するものではありません。
主に、より効率的である可能性があるためです。使用しない場合、値を計算する必要はありません。たとえば、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]を返します)。遅延評価のため、残りはソートされず、計算時間が大幅に節約されます。
これはもちろん非常に単純な例ですが、怠inessは非常に大きいプログラムでも同じように機能します。
ただし、これらすべてにマイナス面があります。プログラムの実行速度とメモリ使用量を予測するのが難しくなります。これは、遅延プログラムが遅くなったり、メモリを消費したりすることを意味するものではありませんが、知っておくと良いでしょう。
遅延評価は多くのことに役立ちます。
まず、すべての既存の遅延言語は純粋です。遅延言語の副作用について考えるのは非常に難しいからです。
純粋な言語では、等式推論を使用して関数定義について推論できます。
foo x = x + 3
残念ながら、非遅延設定では、遅延設定よりも多くのステートメントが返されないため、MLのような言語ではあまり役に立ちません。しかし、怠zyな言語では、平等について安全に推論できます。
第二に、MLの「値制限」のような多くのものは、Haskellのような遅延言語では必要ありません。これにより、構文が大きく整理されます。 MLのような言語では、varやfunなどのキーワードを使用する必要があります。 Haskellでは、これらのことは1つの概念に崩壊します。
第三に、遅延により、部分的に理解できる非常に機能的なコードを作成できます。 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の怠 'な「クイックソート」は1行で、最初のいくつかのアイテムだけを見ると、それらのアイテムだけを選択するコストに比例したコストしか支払わないという利点があります。これを厳密に行うことを妨げるものは何もありませんが、同じ漸近的なパフォーマンスを達成するために、毎回アルゴリズムを再コーディングする必要があります。
5番目に、遅延により、言語で新しい制御構造を定義できます。厳密な言語のコンストラクトのような新しい「if .. then .. else ..」を書くことはできません。次のような関数を定義しようとすると:
if' True x y = x
if' False x y = y
厳密な言語では、条件値に関係なく両方のブランチが評価されます。ループを考慮すると悪化します。すべての厳格なソリューションでは、言語が何らかの引用または明示的なラムダ構築を提供することを必要とします。
最後に、同じ流れで、モナドなどの型システムでの副作用を処理するための最良のメカニズムのいくつかは、実際には怠zyな設定でのみ効果的に表現できます。これは、F#のワークフローの複雑さとHaskell Monadsを比較することで確認できます。 (モナドは厳密な言語で定義できますが、残念ながら怠とワークフローの不足のために1つか2つのモナドの法則に失敗することがよくあります。
通常の順序評価と遅延評価(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表記では)熱心な評価と同等でありながら、より強力になります(評価は他の戦略よりも頻繁に終了します)。
RAMに関連するガベージコレクションと同じ方法で、CPUに関連する遅延評価。 GCを使用すると、メモリの量に制限がないことを装って、必要なだけメモリ内のオブジェクトを要求できます。ランタイムは、使用できないオブジェクトを自動的に回収します。 LEを使用すると、無制限の計算リソースがあるように見せかけることができます。必要な数の計算を実行できます。ランタイムは、不必要な(特定のケースでは)計算を実行しません。
これらの「ふり」モデルの実際的な利点は何ですか?開発者を(ある程度)リソースの管理から解放し、ソースから定型コードを削除します。しかし、より重要なことは、ソリューションをより幅広いコンテキストで効率的に再利用できることです。
番号Sと番号Nのリストがあるとします。リストSから番号Nに最も近い番号Mを見つける必要があります。単一のNとNのリストL(Lの各Nのei Sで最も近いMを検索します)。遅延評価を使用する場合、Sをソートし、バイナリ検索を適用してNに最も近いMを見つけることができます。適切な遅延ソートを行うには、単一のNおよびOに対してO(size(S))ステップが必要です(ln(size(S))*(size(S)+ size(L)))均等に分散されたLの手順。最適な効率を達成するために遅延評価がない場合は、各コンテキストにアルゴリズムを実装する必要があります。
サイモン・ペイトン・ジョーンズを信じるなら、怠evaluationな評価は重要ではありませんそれ自体ではなく、デザイナーが言語を純粋に保つことを余儀なくされた「ヘアシャツ」としてのみです。私はこの観点に共感しています。
リチャード・バード、ジョン・ヒューズ、そして少しはラルフ・ヒンズは、怠zyな評価で驚くべきことをすることができます。彼らの作品を読むことはあなたがそれを感謝するのに役立ちます。適切な出発点は、 Birdの壮大な数独 ソルバーと Why Functional Programming Matters に関するHughesの論文です。
三目並べプログラムを検討してください。これには4つの機能があります。
これにより、懸念が明確に分離されます。特に、ゲームのルールを理解する必要があるのは、ムーブ生成機能とボード評価機能のみです。ムーブツリーとミニマックス機能は完全に再利用可能です。
チックタックトーの代わりにチェスを実装してみましょう。 「熱心な」(つまり従来の)言語では、移動ツリーがメモリに収まらないため、これは機能しません。そのため、ミニマックスロジックを使用して生成するムーブを決定する必要があるため、ボード評価およびムーブ生成機能をムーブツリーおよびミニマックスロジックと混在させる必要があります。すてきなきれいなモジュール構造が消えます。
ただし、遅延言語では、移動ツリーの要素はミニマックス関数からの要求に応じてのみ生成されます。ミニマックスを最上部の要素で解放する前に、移動ツリー全体を生成する必要はありません。したがって、クリーンなモジュール構造は実際のゲームでも機能します。
ここで、議論でまだ取り上げられていないと思われるもう2つのポイントを示します。
遅延は、並行環境における同期メカニズムです。これは、計算への参照を作成し、その結果を多くのスレッド間で共有するための軽量で簡単な方法です。複数のスレッドが未評価の値にアクセスしようとすると、そのうちの1つだけが実行し、他のスレッドはそれに応じてブロックし、値が使用可能になるとその値を受け取ります。
怠azineは、純粋な設定でデータ構造を償却するための基本です。これは、岡崎によるPurely Functional Data Structuresで詳細に説明されていますが、基本的な考え方は、遅延評価は特定のタイプのデータ構造を効率的に実装できるようにするために重要な制御された形式の突然変異であるということです。私たちはしばしば純粋なヘアシャツを着用することを強いる怠について話しますが、他の方法も当てはまります:それらは相乗的な言語機能のペアです。
コンピューターの電源を入れて、WindowsがWindowsエクスプローラーでハードドライブ上のすべてのディレクトリを開くことを控え、コンピューターにインストールされているすべてのプログラムを起動することを控えるとき、特定のディレクトリが必要か特定のプログラムが必要であると示すまで、 「怠lazな」評価です。
「遅延」評価は、必要なときに必要なときに操作を実行します。プログラミング言語またはライブラリの機能である場合、すべてを事前に計算するよりも、独自に遅延評価を実装するのが一般に難しいため、便利です。
効率を高めることができます。これは明らかな外観ですが、実際には最も重要ではありません。 (怠はkill効率でもあります-この事実はすぐにはわかりません。ただし、すぐに計算するのではなく一時的な結果を大量に保存することで、膨大なRAMを使い果たすことができます。)
言語にハードコーディングされるのではなく、通常のユーザーレベルのコードでフロー制御構造を定義できます。 (たとえば、Javaにはfor
ループがあり、Haskellにはfor
関数があります。Javaには例外処理があります。Haskellにはさまざまな例外モナドの種類。C#にはgoto
があり、Haskellには継続モナドがあります...)
生成データのアルゴリズムを、生成する量データの決定アルゴリズムから分離できます。結果の概念的に無限のリストを生成する1つの関数と、必要に応じてこのリストを処理する別の関数を作成できます。さらに重要なのは、fiveジェネレーター関数とfiveコンシューマー関数があり、組み合わせて5 x 5 = 25の関数を手動でコーディングする代わりに、任意の組み合わせを効率的に生成できることです。両方のアクションを一度に。 (!)デカップリングは良いことです。
多かれ少なかれ、純粋な関数型言語を設計することを余儀なくされます。ショートカットを使用することは常に魅力的ですが、怠zyな言語では、ごくわずかな不純物がコードをwildly予測不能にします。これは、ショートカットの取得を強く妨げます。
このことを考慮:
if (conditionOne && conditionTwo) {
doSomething();
}
メソッドdoSomething()は、conditionOneがtrueの場合にのみ実行されますand conditionTwoがtrueです。 conditionOneがfalseの場合、なぜconditionTwoの結果を計算する必要があるのですか? conditionTwoの評価は、この場合、特に条件が何らかのメソッドプロセスの結果である場合、時間の無駄になります。
これは、遅延評価の関心の一例です...
遅延の大きな利点の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(length of 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 and appending。をサポートします。興味深いことに、ノードがアクセスされるまでノードを評価しないため、潜在的に無限の要素を持つリストを完全に構築できます。
上記のデータ構造では、各トラバーサルでノードを再計算する必要がないため、.NETのVanilla IEnumerablesとは明らかに異なります。
遅延評価は、データ構造で最も役立ちます。構造内の特定の点のみを帰納的に指定し、配列全体で他のすべての点を表現する配列またはベクトルを定義できます。これにより、非常に簡潔かつ高い実行時パフォーマンスでデータ構造を生成できます。
これを実際に見るには、 instinct というニューラルネットワークライブラリをご覧ください。それは優雅さと高性能のために遅延評価を多用します。たとえば、私は伝統的に命令型のアクティベーション計算を完全に取り除きます。単純な怠expressionな表現は私のためにすべてを行います。
これは、たとえば アクティベーション関数 で使用され、逆伝播学習アルゴリズムでも使用されます(2つのリンクしか投稿できないため、_learnPat
関数を_AI.Instinct.Train.Delta
_モジュール自身)。従来、どちらもはるかに複雑な反復アルゴリズムを必要とします。
このスニペットは、遅延評価と遅延評価の違いを示しています。もちろん、このフィボナッチ関数自体を最適化し、再帰の代わりに遅延評価を使用することもできますが、これは例を損ないます。
[〜#〜] 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)
他の人々はすでにすべての大きな理由を挙げましたが、怠lazが重要な理由を理解するのに役立つ有用な練習は、厳密な言語で fixed-point 関数を試して書くことだと思います。
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
がレイジーであることです。すでに厳格なf
が与えられたら、空中に手を放してあきらめるか、イータを広げて物を散らかすことができます。 (これは、Noahがlibraryであり、言語ではなく厳格であると言っていることとよく似ています)。
次に、厳密な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)では、必要に応じて厳密性を要求できます。したがって、言語を選択しても実際にプログラムが遅延したり遅延したりすることはありませんが、デフォルトで取得するものに影響するだけです。
そのように考えたら、後でデータを生成するために使用できるデータ構造を作成するすべての場所を考えてください(その前にあまり見ないで)。評価。
遅延評価なしでは、次のような記述は許可されません。
if( obj != null && obj.Value == correctValue )
{
// do smth
}
私が使用した遅延評価の最も有用な活用は、特定の順序で一連のサブ関数を呼び出す関数でした。これらのサブ関数のいずれかが失敗した(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;
}
使用を開始すると、より頻繁に使用する機会が得られます。
とりわけ、遅延言語では、多次元の無限データ構造が許可されます。
スキーム、Pythonなどでは、ストリームを使用して1次元の無限データ構造を使用できますが、1次元に沿ってのみトラバースできます。
怠azineは 同じフリンジ問題 には便利ですが、そのリンクで言及されているコルーチンの接続に注目する価値があります。
遅延評価は、貧乏人の等式推論です(理想的には、関係する型と操作のプロパティからコードのプロパティを推測することが期待できます)。
うまく機能する例:sum . take 10 $ [1..10000000000]
。単純で単純な数値計算を1つだけではなく、合計で10個の数値に減らすことは問題ありません。もちろん遅延評価がなければ、これは最初の10個の要素を使用するためだけにメモリ内に巨大なリストを作成します。それは確かに非常に遅く、メモリ不足エラーを引き起こす可能性があります。
望んでいるほど大きくない例:sum . take 1000000 . drop 500 $ cycle [1..20]
。リストではなくループ内であっても、実際には1 000 000の数値を合計します。それでもは、条件と数式がほとんどない、1つの直接的な数値計算に減らす必要があります。どのが1 000 000の数字を合計するよりもはるかに良いでしょう。リスト内ではなく、ループ内であっても(つまり、森林破壊の最適化後)。
もう1つは、 tail recursion modulo cons スタイルでコーディングすることを可能にし、だけで動作します。
cf. 関連する回答 。
「遅延評価」によって、結合されたブール値のように、
if (ConditionA && ConditionB) ...
答えは、プログラムが消費するCPUサイクルが少ないほど、実行が速くなるということです...そして、処理命令のチャンクがプログラムの結果に影響を与えない場合、それは不必要です(したがって無駄です)とにかくそれらを実行する...
もしそうなら、あなたは私が「怠asな初期化子」として知っているものを意味します:
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未満のすべての数値のリストを作成します。次に、述語でフィルター処理します。数値は降順で並べ替えられるため、述語を満たす最大の数値はフィルター処理されたリストの最初の要素です。開始セットに有限リストを使用する必要さえありませんでした。それは再び行動の怠inessです。最終的にフィルター処理されたリストの先頭を使用するだけなので、フィルター処理されたリストが有限か無限かは関係ありません。最初の適切な解決策が見つかると、評価は停止します。