なぜ遅延評価が役立つのですか?


回答:


96

主にそれがより効率的である可能性があるため-値が使用されない場合、値を計算する必要はありません。たとえば、3つの値を関数に渡すことができますが、条件式のシーケンスによっては、実際にはサブセットのみが使用される場合があります。Cのような言語では、3つの値はすべてとにかく計算されます。しかしHaskellでは、必要な値のみが計算されます。

それはまた、無限リストのようなクールなものを可能にします。Cのような言語では無限のリストを作成することはできませんが、Haskellでは問題ありません。無限リストは、数学の特定の領域でかなり頻繁に使用されるため、それらを操作する機能があると便利です。


6
Pythonはイテレータを介して遅延リストを無限に評価しました
Mark Cidade、

4
ジェネレーターとジェネレーター式(リスト内包と同様の方法で機能)を使用して、Pythonで無限リストを実際にエミュレートできます:python.org/doc/2.5.2/ref/genexpr.html
John Montgomery

24
ジェネレーターを使用すると、Pythonでレイジーリストを簡単に作成できますが、他のレイジー評価手法やデータ構造は著しくエレガントではありません。
Peter Burns

3
私はこの答えに同意しないと思います。怠惰は効率に関するものであると以前は考えていましたが、Haskellをかなりの量使用し、Scalaに切り替えて経験を比較したところ、効率のために怠惰が重要であることがほとんどありませんでした。エドワード・クメットは本当の理由を思いついたと思います。
オーウェン

3
同様に私は同意しませんが、厳密な評価のためにCには無限リストの明示的な概念はありませんが、サンクを使用して関数を渡すことで、他の言語で(そして実際、ほとんどすべての遅延言語の実際の実装で)同じトリックを簡単にプレイできます同様の式によって生成された無限構造の有限接頭辞を処理するためのポインタ。
Kristopher Micinski 2012

71

遅延評価の有用な例は、次の使用です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]を返します)、すでに十分です。遅延評価のため、残りはソートされないため、計算時間を大幅に節約できます。

これはもちろん非常に単純な例ですが、遅延は非常に大きなプログラムでも同じように機能します。

ただし、これらすべての欠点があります。プログラムのランタイム速度とメモリ使用量を予測することが難しくなります。これは、遅延プログラムが遅くなる、またはより多くのメモリを使用することを意味するものではありませんが、知っておくとよいでしょう。


19
より一般的には、take k $ quicksort listO(n + k log k)時間しかかかりませんn = length list。非遅延比較ソートでは、これは常にO(n log n)時間かかります。
ephemient

@ephemient O(nk log k)を意味するのではないですか?
MaiaVictor 2013年

1
@Viclibいいえ、私が言ったことを意味しました。
ephemient 2013年

@ephemientでは、悲しいことに、私はそれを理解できないと思います
MaiaVictor 2013年

2
@Viclib nから上位k要素を見つけるための選択アルゴリズムは、O(n + k log k)です。レイジー言語でクイックソートを実装し、最初のk個の要素を決定するのに十分なだけそれを評価する場合(後に評価を停止)、非レイジー選択アルゴリズムとまったく同じ比較を行います。
ephemient 2013年

70

遅延評価は多くのことに役立ちます。

まず、既存の遅延言語はすべて純粋です。遅延言語の副作用について考えるのは非常に難しいためです。

純粋な言語では、方程式の推論を使用して関数の定義を推論できます。

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つのモナドの法則が失敗することが多く、厳密な手荷物を大量に受け取ります。)


5
非常に素晴らしい; これらが本当の答えです。私は、Haskellをかなりの量使用し、それが本当に理由ではないことを確認するまでは、効率(後で計算を遅らせること)についてだと考えていました。
オーウェン、

11
また、怠惰な言語が純粋でなければならないことは技術的には正しくありませんが(例としてR)、不純な怠惰な言語は非常に奇妙なことを行うことができます(例としてR)。
オーエン、

4
確かにあります。厳密な言語でletは、再帰は危険な獣です。R6RSスキームで#fは、結び目を結ぶことがサイクルにつながるどこにでも、ランダムに単語を出現させます!しゃれは意図されていませんletが、遅延言語では厳密にもっと再帰的なバインディングが賢明です。where厳密さは、SCCを除いて、相対的な効果を順序付ける方法がないという事実をさらに悪化させます。これはステートメントレベルの構造であり、その効果は厳密に任意の順序で発生する可能性があり、純粋な言語であっても、#f問題。厳格なwhere非ローカルの懸念でコードをなぞります。
エドワードKMETT 2012年

2
遅延が値の制限を回避するのにどのように役立つか説明できますか?これを理解することができませんでした。
トム・エリス

3
@PaulBone何言ってるの?怠惰は、制御構造とかなり関係があります。厳密な言語で独自の制御構造を定義する場合、ラムダなどの束を使用する必要があります。そうしないと、うまくいきません。なぜならifFunc(True, x, y)両方xではyなく単に評価するためですx
セミコロン

28

通常の順序の評価と遅延評価(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表記では)熱心な評価と同等でありながら、より強力になります(評価は他の戦略よりも頻繁に終了します)。


25

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のステップ。最適な効率を達成するための遅延評価がない場合は、各コンテキストにアルゴリズムを実装する必要があります。


GCとの類似性は私に少し役立ちましたが、「定型コードを削除する」の例を挙げていただけますか?
Abdul

1
@Abdul、あらゆるORMユーザーに馴染みのある例:遅延関連付けロード。「ジャストインタイム」でDBから関連付けをロードし、同時に、いつそれをロードするか、およびどのようにキャッシュするかを明示的に指定する必要から開発者を解放します(これはボイラープレートです)。次に別の例を示します:projectlombok.org/features/GetterLazy.html
Alexey、

25

サイモンペイトンジョーンズを信じている場合、怠惰な評価はそれ自体重要ではなく、デザイナーに言語を純粋に保つことを強いた「シャツ」としてのみ重要です。私はこの観点に共感しています。

リチャード・バード、ジョン・ヒューズ、そして多少なりとも、ラルフ・ヒンズは遅延評価で驚くべきことを行うことができます。彼らの作品を読むことはあなたがそれを理解するのに役立ちます。良い出発点は Birdの素晴らしい数独ソルバーとHughesの「関数型プログラミングが重要である理由」に関する論文があります。


それは彼らに言語を純粋に保つこと強制しただけでなく、それはまた(モナドの導入前に)の署名がそうであり、あなたがすでに適切な対話型プログラムを書くことができたときにそうすることを彼らに許可しました。IOmainString -> String
leftaroundabout

@leftaroundabout:厳密な言語がすべての効果をIOモナドに強制するのを止めているのは何ですか?
トム・エリス

13

三目並べプログラムを考えてみましょう。これには4つの機能があります。

  • 現在のボードを取得し、1つの移動が適用された新しいボードのリストを生成する移動生成関数。
  • 次に、「移動ツリー」関数があり、移動生成関数を適用して、これから続く可能性のあるすべての可能なボード位置を導き出します。
  • 最適な次の移動を見つけるためにツリー(またはおそらくその一部のみ)を歩くミニマックス関数があります。
  • プレイヤーの1人が勝ったかどうかを決定するボード評価関数があります。

これにより、懸念が明確に分離されます。特に、移動生成関数とボード評価関数は、ゲームのルールを理解する必要がある唯一の関数です。移動ツリーとミニマックス関数は完全に再利用可能です。

次に、tic-tac-toeの代わりにチェスを実装してみましょう。「熱心な」(つまり従来の)言語では、移動ツリーがメモリに収まらないため、これは機能しません。したがって、ボードの評価および移動生成関数は、移動する移動を決定するためにミニマックスロジックを使用する必要があるため、移動ツリーおよびミニマックスロジックと混合する必要があります。きれいなモジュール構造がなくなります。

ただし、遅延言語では、移動ツリーの要素はminimax関数からの要求に応答してのみ生成されます。最上位の要素でminimaxを解放する前に、移動ツリー全体を生成する必要はありません。したがって、クリーンなモジュール構造は実際のゲームでも機能します。


1
[「熱心な」(つまり、従来の)言語では、移動ツリーがメモリに収まらないため、これは機能しません]-Tic-Tac-Toeの場合は、確実に機能します。保存できるのは最大3 ** 9 = 19683ポジションです。それぞれを贅沢な50バイトに格納すると、ほぼ1メガバイトになります。それは何でもない...
ジョナス・ケルカー09年

6
はい、それが私のポイントです。熱心な言語は、ささいなゲームにはすっきりとした構造を持つことができますが、現実のものには妥協する必要があります。怠惰な言語にはその問題はありません。
ポールジョンソン

3
ただし、公平に言えば、遅延評価はそれ自体のメモリの問題につながる可能性があります。熱心な評価で、メモリ消費量がO(1)の何かのメモリである理由をhaskellが吹き飛ばしている理由を人々が尋ねるのは珍しいことではありません
RHSeeger

@PaulJohnsonすべての位置を評価する場合、それらを熱心に評価するか、遅延して評価するかに関係なく、違いはありません。同じ作業を行う必要があります。途中で停止して位置の半分だけを評価しても、どちらの場合も作業の半分を実行する必要があるため、違いはありません。2つの評価の唯一の違いは、遅延して記述した場合、アルゴリズムの見栄えが良くなることです。
ceving

12

ここでは、まだ議論で取り上げられていないと思われる2つのポイントを示します。

  1. 怠惰は、並行環境における同期メカニズムです。これは、いくつかの計算への参照を作成し、その結果を多くのスレッド間で共有するための軽量で簡単な方法です。複数のスレッドが未評価の値にアクセスしようとすると、そのうちの1つだけがそれを実行し、他のスレッドはそれに応じてブロックし、値が利用可能になると値を受け取ります。

  2. 怠惰は、純粋な設定でデータ構造を償却するための基本です。これは岡崎によって純粋に機能的なデータ構造で詳細に説明されていますが、基本的な考え方は、遅延評価は特定のタイプのデータ構造を効率的に実装するために重要な突然変異の制御された形式であるということです。私たちはしばしば、私たちが純粋なヘアシャツを着用することを強いる怠惰について話しますが、逆の方法も適用されます。それらは、相乗的な言語機能のペアです。


10

コンピューターの電源を入れ、Windowsがエクスプローラーでハードドライブのすべての単一のディレクトリを開かないようにし、コンピューターにインストールされているすべての単一のプログラムを起動しないようにすると、特定のディレクトリが必要であるか、特定のプログラムが必要であることが示されるまで、 「遅延」評価です。

「遅延」評価とは、必要なときに必要なときに操作を実行することです。これは、プログラミング言語またはライブラリの機能である場合に役立ちます。通常、事前にすべてを事前に計算するよりも、遅延評価を自分で実装することが難しいからです。


1
一部の人々は、それが本当に「怠惰な実行」であると言うかもしれません。Haskellのような合理的に純粋な言語を除いて、違いは本当に重要ではありません。ただし、違いは、計算が遅れるだけでなく、それに関連する副作用(ファイルのオープンや読み取りなど)でもあるということです。
オーウェン、

8

このことを考慮:

if (conditionOne && conditionTwo) {
  doSomething();
}

メソッドdoSomething()は、conditionOneがtrueで conditionTwoがtrueの場合にのみ実行されます。conditionOneがfalseの場合、なぜconditionTwoの結果を計算する必要があるのですか?この場合、特に条件が何らかのメソッドプロセスの結果である場合、conditionTwoの評価は時間の無駄になります。

これは遅延評価の興味の一例です...


それは怠惰な評価ではなく、短絡的なものだと思いました。
Thomas Owens、

2
conditionTwoは本当に必要な場合(つまり、conditionOneがtrueの場合)にのみ計算されるため、これは遅延評価です。
Romain Linsolas 2008年

7
ショートサーキットは遅延評価の退化したケースだと思いますが、それを考える一般的な方法ではありません。
rmeador 2008年

19
短絡は、実際には遅延評価の特殊なケースです。遅延評価には、明らかに、短絡だけではありません。または、短絡は遅延評価に加えて何を持っていますか?
yfeldblum 2008

2
@ジュリエット:あなたは「怠惰」の強い定義を持っています。2つのパラメーターを取る関数の例は、短絡ifステートメントと同じではありません。短絡ifステートメントは、不要な計算を回避します。あなたの例とのより良い比較は、両方の条件を強制的に評価するVisual Basicの演算子「andalso」であると思います

8
  1. 効率を高めることができます。これは明白に見えるものですが、実際には最も重要ではありません。(注も怠惰ができることを殺しすぎて効率を-この事実はすぐに明らかにされていませんが、一時的な結果の多くを保存するのではなく、すぐにそれらを計算することで、あなたはRAMの膨大な量を使用することができます。)

  2. 言語にハードコードするのではなく、通常のユーザーレベルのコードでフロー制御構造を定義できます。(たとえば、Javaにはforループがあります。Haskellにはfor関数があります。Javaには例外処理があります。Haskellにはさまざまなタイプの例外モナドがあります。C#にはgoto; Haskellには継続モナドがあります...)

  3. 生成するデータを決定するアルゴリズムから、データを生成するアルゴリズムを分離できます。結果の概念的に無限のリストを生成する1つの関数と、このリストを必要と判断しただけ処理する別の関数を作成できます。さらに言えば、5つのジェネレーター関数と5つのコンシューマー関数を使用でき、両方のアクションを一度に組み合わせる5 x 5 = 25関数を手動でコーディングする代わりに、任意の組み合わせを効率的に生成できます。(!)デカップリングが良いことであることは誰もが知っています。

  4. それは多かれ少なかれ純粋な関数型言語を設計することを強制します。常にショートカットを取るのは魅力的ですが、怠惰な言語では、わずかな不純物によってコードが非常に予測不可能になり、ショートカットを取るのを強く抑えます。


6

遅延の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とは明らかに異なります。


5

このスニペットは、遅延評価と遅延評価の違いを示しています。もちろん、このフィボナッチ関数自体を最適化して、再帰の代わりに遅延評価を使用することもできますが、これでは例が台無しになります。

我々は仮定しよう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)

5

遅延評価は、データ構造で最も役立ちます。構造内の特定のポイントのみを誘導的に指定し、配列全体に関して他のすべてを表現する配列またはベクトルを定義できます。これにより、非常に簡潔かつ高い実行時パフォーマンスでデータ構造を生成できます。

この動作を確認するには、本能と呼ばれる私のニューラルネットワークライブラリをご覧ください。優雅さと高性能のために遅延評価を多用します。たとえば、私は伝統的に命令型アクティベーション計算を完全に取り除きます。単純な怠惰な表現は私のためにすべてを行います。

これは、たとえばアクティベーション関数やバックプロパゲーション学習アルゴリズムで使用されます(私は2つのリンクしか投稿できないのでlearnPatAI.Instinct.Train.Delta自分でモジュールで関数を検索する必要があります)。従来はどちらも、はるかに複雑な反復アルゴリズムが必要です。


4

他の人々はすでに大きな理由をすべて述べましたが、怠惰が重要である理由を理解するのに役立つ有用な演習は、厳密な言語で固定小数点関数を書いてみることです。

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)

3

あなたが現在どのように考えているのかはわかりませんが、遅延評価を言語機能ではなくライブラリの問題と考えると便利だと思います。

つまり、厳密な言語では、いくつかのデータ構造を構築することで遅延評価を実装でき、遅延言語(少なくともHaskell)では、必要なときに厳密さを要求できます。したがって、言語の選択によってプログラムが遅延することも遅延しないこともありませんが、デフォルトで取得するプログラムに影響を与えるだけです。

そのように考えたら、後でデータを生成するために使用できるデータ構造を作成するすべての場所を考えてください(その前にあまり詳しく調べることなく)。遅延の多くの使用法がわかります。評価。


1
厳密な言語で遅延評価を実装することは、多くの場合、チューリングターピットです。
itsbruce、2012年

2

私が使用した遅延評価の最も有用な活用は、特定の順序で一連のサブ関数を呼び出す関数でした。これらのサブ関数のいずれかが失敗した(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;
}

使い始めると、ますます頻繁に使用する機会が見えてきます。


2

遅延評価なしでは、次のようなものを書くことはできません:

  if( obj != null  &&  obj.Value == correctValue )
  {
    // do smth
  }

まあ、いいえ、これはそれを行うには悪い考えです。このコードは(達成しようとしていることによっては)正しい場合もありますが、読むのは難しく、これは常に悪いことです。
ブラン

12
そうは思いません。そのCとその親戚の標準的な構造。
ポールジョンソン

これは遅延評価の例であり、遅延評価ではありません。それとも実質的に同じことですか?
RufusVS

2

とりわけ、遅延言語は多次元の無限データ構造を可能にします。

スキーム、Pythonなどでは、ストリームを含む1次元の無限データ構造が許可されていますが、トラバースできるのは1次元のみです。

怠惰は同じフリンジ問題にも役立ちますが、そのリンクで言及されているコルーチンの接続に注目する価値があります。


2

遅延評価は貧弱な人の方程式の推論です(これは、理想的には、関係する型のプロパティと操作のコードからコードのプロパティを推定することであると期待できます)。

うまく機能する例:sum . take 10 $ [1..10000000000]。直接で単純な数値計算を1つだけ行うのではなく、合計を10の数に減らしてもかまいません。もちろん、遅延評価がなければ、最初の10個の要素を使用するだけで、メモリ内に巨大なリストが作成されます。確かに非常に遅くなり、メモリ不足エラーが発生する可能性があります。

私たちが好きな、それは素晴らしいようではありません。例:sum . take 1000000 . drop 500 $ cycle [1..20]。これは、リスト内ではなくループ内であっても、実際には1 000 000の数値を合計します。それでも、条件と式がほとんどない、直接の数値計算を1つに減らす必要があります。これ、1 000 000の数値を合計するよりもはるかに優れています。ループ内であっても、リスト内ではない場合(つまり、森林破壊最適化後)。


もう一つは、それは、コードすることを可能にする末尾再帰法と短所のスタイル、そしてそれはちょうど働きます

cf. 関連回答


1

「遅延評価」とは、次のような結合ブール値のようなものを意味します

   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)); 
          } 
    }
}

まあ、この手法により、クラスを使用するクライアントコードは、従業員オブジェクトを使用するクライアントがスーパーバイザーのデータへのアクセスを必要とする場合を除いて、スーパーバイザーデータレコードのデータベースを呼び出す必要を回避できます...これにより、従業員のインスタンス化のプロセスが速くなり、さらに、スーパーバイザーが必要な場合は、スーパーバイザープロパティへの最初の呼び出しでデータベース呼び出しがトリガーされ、データがフェッチされて使用可能になります...


0

高次関数からの抜粋

3829で割り切れる100,000未満の最大数を見つけましょう。これを行うには、解が存在することがわかっている可能性のセットをフィルター処理するだけです。

largestDivisible :: (Integral a) => a  
largestDivisible = head (filter p [100000,99999..])  
    where p x = x `mod` 3829 == 0 

最初に、100,000未満のすべての数値のリストを降順で作成します。次に、それを述語でフィルター処理します。数値は降順で並べ替えられるため、述語を満たす最大の数は、フィルター処理されたリストの最初の要素です。開始セットに有限のリストを使用する必要さえありませんでした。それは再び怠惰です。フィルターされたリストの先頭のみを使用することになるため、フィルターされたリストが有限であるか無限であるかは問題ではありません。最初の適切な解が見つかると、評価は停止します。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.