web-dev-qa-db-ja.com

Haskell:非厳密と怠惰はどう違うのですか?

lazynon-strictと同じではないことをよく読みますが違いがわかりにくいです。それらはらしい同じ意味で使用されますが、意味が異なることを理解しています。違いを理解するのに助けていただければ幸いです。

この投稿について散在しているいくつかの質問があります。これらの質問をこの投稿の最後に要約します。スニペットの例がいくつかありますが、テストはせず、概念としてのみ提示しました。引用符を追加して、検索しないようにしました。多分それは同じ質問で後で誰かを助けるでしょう。

非厳密な定義:

関数fは、終了しない式に適用されたときに、それも終了しない場合、厳密であると言われます。言い換えると、fbotの値がである場合、fは厳密です。ほとんどのプログラミング言語では、すべての関数が厳密です。しかし、これはHaskellではそうではありません。簡単な例として、次のように定義される定数1関数const1について考えてみます。

const1 x = 1

Haskellのconst1ボットの値は1です。操作上、const1は引数の値を「必要としない」ため、評価を試みず、終了しない計算に巻き込まれることはありません。このため、非正格関数は「レイジー関数」とも呼ばれ、引数を「レイジー」または「必要に応じて」評価すると言われています。

- Haskellの穏やかな紹介:関数

私はこの定義が本当に好きです。厳密に理解するために私が見つけることができる最高のもののようです。 const1 x = 1も怠け者ですか?

非厳密性とは、削減(評価の数学用語)が外部から進行することを意味します。

したがって、(a +(b c))がある場合は、最初に+を減らし、次に内側(b c)を減らします。

- Haskell Wiki:Lavy vs non-strict

HaskellWikiは本当に私を混乱させます。彼らが注文について言っていることは理解していますが、(a+(b*c))が合格した場合、_|_がどのように非厳密に評価されるかわかりませんか?

非厳密な評価では、関数の引数は、関数本体の評価で実際に使用されない限り評価されません。

チャーチエンコーディングでは、演算子の遅延評価は関数の非厳密な評価にマッピングされます。このため、厳密でない評価は「レイジー」と呼ばれることがよくあります。多くの言語のブール式は、短絡評価と呼ばれる非厳密な評価の形式を使用します。この場合、明確なブールが結果として生じると判断されるとすぐに評価が返されます。たとえば、trueが検出された論理和式、またはfalseが検出された場合の論理和式など。条件式も通常、遅延評価を使用します。遅延評価では、明確な分岐が発生するとすぐに評価が返されます。

- ウィキペディア:評価戦略

怠惰な定義:

一方、遅延評価とは、結果が必要な場合にのみ式を評価することを意味します(「削減」から「評価」への移行に注意してください)。したがって、評価エンジンは式を検出すると、式を評価するために必要な値と、式自体へのポインターを含むサンクデータ構造を構築します。結果が実際に必要になると、評価エンジンは式を呼び出し、後で参照できるようにサンクを結果に置き換えます。 .。

明らかに、サンクと部分的に評価された表現の間には強い対応関係があります。したがって、ほとんどの場合、「遅延」と「非厳密」という用語は同義語です。しかし、完全ではありません。

- Haskell Wiki:Lavy vs non-strict

これはHaskell固有の答えのようです。 lazyはサンクを意味し、non-strictは部分評価を意味すると思います。その比較は単純すぎますか? lazyは常にサンクを意味し、non-strictは常に部分評価を意味しますか?.

プログラミング言語理論では、遅延評価またはcall-by-need 1 は、式の値が実際に必要になるまで(厳密でない評価)、式の評価を遅らせ、繰り返しの評価を回避する評価戦略です(共有)。

- ウィキペディア:遅延評価

命令例

関数型言語を学ぶとき、ほとんどの人が命令型プログラミングを忘れると言うことを私は知っています。しかし、これらが非厳密、怠惰、両方、またはどちらにも該当しないかどうかを知りたいですか?少なくとも、それはなじみのあるものを提供するでしょう。

短絡

f1() || f2()

C#、Pythonおよび「yield」を使用する他の言語

public static IEnumerable Power(int number, int exponent)
{
    int counter = 0;
    int result = 1;
    while (counter++ < exponent)
    {
        result = result * number;
        yield return result;
    }
}

- MSDN:yield(c#)

コールバック

int f1() { return 1;}
int f2() { return 2;}

int lazy(int (*cb1)(), int (*cb2)() , int x) {
    if (x == 0)
        return cb1();
    else
        return cb2();
}

int eager(int e1, int e2, int x) {
    if (x == 0)
         return e1;
    else
         return e2;
}

lazy(f1, f2, x);
eager(f1(), f2(), x);

質問

私は答えがそれらすべてのリソースで私の目の前にあることを知っています、しかし私はそれを理解することができません。定義が暗示的または明白であるとして簡単に却下されすぎているように思われます。

私はたくさんの質問があることを知っています。関連性があると思われる質問には、遠慮なくお答えください。私はそれらの質問を議論のために追加しました。

  • const1 x = 1も怠け者ですか?
  • 「内向き」からの評価はどのように厳密ではありませんか? const1 x = 1のように、内向きに不要な表現を減らすことができるからですか?削減は怠惰の定義に合うようです。
  • lazyは常にthunksを意味しますか)non-strictは常に部分評価を意味しますか?これは単なる一般化ですか?
  • 次の命令型の概念は、怠惰、非厳密、両方、またはどちらでもないですか?
    • 短絡
    • 歩留まりの使用
    • 実行を遅延または回避するためのコールバックの受け渡し
  • lazynon-strictのサブセットであるか、またはその逆であるか、またはそれらは相互に排他的です。たとえば、lazyでなくても、non-strictになることは可能ですか?またはlazynon-strictではありませんか?
  • Haskellの非厳格さは怠惰によって達成されますか?

ありがとうSO!

66
user295190

非厳密で怠惰なものは、非公式に交換可能ですが、さまざまな議論の領域に適用されます。

Non-strictsemantics を指します:式の数学的意味。非厳密が適用される世界には、関数の実行時間、メモリ消費、さらにはコンピュータの概念がありません。ドメイン内のどの種類の値がコドメイン内のどの種類の値にマップされるかについて説明するだけです。特に、strict関数は、値⊥( "bottom"-これについては上記のセマンティクスリンクを参照)を⊥にマップする必要があります。厳密でない関数はこれを行わないことが許可されています。

Lazyは、操作上の動作、つまり実際のコンピューターでコードが実行される方法を指します。ほとんどのプログラマーはプログラムを操作上考えているので、これはおそらくあなたが考えていることです。遅延評価とは、サンクを使用した実装を指します。つまり、最初に実行されたときに値に置き換えられるコードへのポインターです。ここで意味のない単語に注意してください:「ポインタ」、「初めて」、「実行済み」。

遅延評価は非厳密なセマンティクスを引き起こします。そのため、概念は非常に密接に見えます。しかし、FUZxxlが指摘しているように、非厳密なセマンティクスを実装する方法は怠惰だけではありません。

この違いについて詳しく知りたい場合は、上記のリンクを強くお勧めします。それを読むことは、コンピュータプログラムの意味についての私の概念のターニングポイントでした。

57
luqui

strictでもlazy:でもない評価モデルの例楽観的な評価、これは多くの「簡単な」サンクを回避できるため、ある程度のスピードアップをもたらします。

楽観的な評価とは、超式を評価するために部分式が必要ない場合でも、いくつかのヒューリスティックを使用してその一部を評価することを意味します。部分式が十分な速さで終了しない場合は、本当に必要になるまで評価を一時停止します。これにより、サンクを生成する必要がないため、後で部分式が必要になった場合に遅延評価よりも有利になります。一方、式が終了しなくても、すぐに中止できるため、あまり失うことはありません。

ご覧のとおり、この評価モデルは厳密ではありません:_ | _を生成するものが評価されても必要ない場合、関数は終了します。エンジンが評価を中止するため。一方、必要以上の式が評価される可能性があるため、not完全にlazy

16
fuz

はい、ここでは用語の使用法が不明確ですが、ほとんどの場合、用語は一致しているため、それほど問題にはなりません。

主な違いの1つは、用語が評価されるときです。これには複数の戦略があり、「できるだけ早く」から「最後の瞬間のみ」までの範囲に及びます。 熱心な評価という用語は、前者に傾いた戦略に使用されることがありますが、遅延評価は、大きく傾いた戦略のファミリーを適切に指します。後者に向けて。 「遅延評価」と関連する戦略の違いは、何かを評価した結果がいつどこで保持されるか、または捨てられるかということです。データ構造に名前を割り当て、それにインデックスを付けるというHaskellでおなじみのメモ化手法は、これに基づいています。対照的に、(「名前による呼び出し」評価のように)単に式を相互に接続した言語は、これをサポートしない可能性があります。

他の違いはどの用語が評価されるか、「絶対にすべて」から「できるだけ少ない」までの範囲です。最終結果の計算に実際に使用される値は無視できないため、ここでの違いは、評価される余分な用語の数です。プログラムが実行しなければならない作業量を減らすだけでなく、未使用の用語を無視することは、それらが生成したであろうエラーが発生しないことを意味します。区別が引かれているとき、strictnessは、検討中のすべてを評価するプロパティを指します(たとえば、厳密な関数の場合、これはそれが適用される用語を意味します。それはしないは必ずしも引数内の部分式を意味します)一方、non-strictは、(評価を遅らせるか、破棄することによって)一部のことだけを評価することを意味します完全に用語)。

これらが複雑な方法でどのように相互作用するかを簡単に確認できるはずです。両極端は互換性がない傾向があるため、決定はまったく直交していません。例えば:

  • 非常に厳密でない評価は、ある程度の熱意を排除します。用語が必要かどうかわからない場合は、まだ評価できません。

  • 非常に厳密な評価により、非熱意はやや無関係になります。すべてを評価している場合、そうするというwhenの決定はそれほど重要ではありません。

ただし、別の定義が存在します。たとえば、少なくともHaskellでは、「厳密な関数」は、引数が評価されるたびに関数が_ | _(「ボトム」)と評価されるように引数を十分に強制する関数として定義されることがよくあります。 id xの結果を強制すると、idだけを強制するのとまったく同じ動作になるため、この定義では、xは(些細な意味で)厳密であることに注意してください。

6
C. A. McCann

これはアップデートとして始まりましたが、長くなり始めました。

Laziness/Call-by-need は、関数の引数が評価される場合の名前による呼び出しのメモ化バージョンです。 、その値は後で使用するために保存されます。 「純粋な」(効果のない)設定では、これは名前による呼び出しと同じ結果を生成します。関数の引数が2回以上使用される場合、ほとんどの場合、必要に応じた呼び出しの方が高速です。
命令例-どうやらこれは可能です。 Lazy Imperative Languages に関する興味深い記事があります。それは2つの方法があると言います。 1つはクロージャが必要で、もう1つはグラフ還元を使用します。 Cはクロージャをサポートしていないため、イテレータに引数を明示的に渡す必要があります。マップ構造をラップして、値が存在しない場合は計算し、そうでない場合は戻り値を返すことができます。
:Haskellは、「最初に実行されたときに値に置き換えられるコードへのポインター」によってこれを実装します-luqui。
これは厳密ではない名前による呼び出しですが、結果の共有/記憶があります。

Call-By-Name -Call-by-Name評価では、関数への引数は関数の前に評価されませんと呼ばれます—むしろ、それらは関数本体に直接置換され(キャプチャ回避置換を使用)、関数に現れるたびに評価されるように残されます。関数本体で引数が使用されていない場合、引数は評価されません。何度も使用した場合は、表示されるたびに再評価されます。
命令例:コールバック
注:使用しない場合は評価を回避するため、これは厳密ではありません。

Non-Strict =非厳密な評価では、関数の引数は、実際に評価で使用されない限り評価されません。関数本体の。
命令例:短絡
:_ | _は、関数が厳密でないかどうかをテストする方法のようです

したがって、関数は厳密ではありませんが、怠惰ではありません。怠惰な関数は常に厳密ではありません。 Call-By-Needは、Call-By-Nameによって部分的に定義されていますこれは部分的にNon-Strictによって定義されます

"Lazy Imperative Languages" からの抜粋

2.1。非厳密なセマンティクスと遅延評価最初に、「非厳密なセマンティクス」と「遅延評価」の違いを明確にする必要があります。非厳密セマンティクスとは、プリミティブ操作で必要になるまで式が評価されないことを指定するものです。さまざまなタイプの非厳密なセマンティクスが存在する可能性があります。たとえば、非厳密なプロシージャコールは、値が必要になるまで引数を評価しません。データコンストラクターには非厳密なセマンティクスがあり、複合データが未評価の部分から組み立てられます。遅延評価とも呼ばれる遅延評価は、非厳密なセマンティクスを実装するために通常使用される手法です。セクション4では、遅延評価を実装するために一般的に使用される2つの方法を非常に簡単に要約します。

CALL BY VALUE、CALL BY LAZY、およびCALL BY NAME「Callbyvalue」は、厳密なセマンティクスを持つプロシージャ呼び出しに使用される一般的な名前です。 valuelanguagesによる呼び出しでは、プロシージャコールの各引数は、プロシージャコールが行われる前に評価されます。次に、値がプロシージャまたはそれを囲む式に渡されます。値による呼び出しの別名は「熱心な」評価です。値による呼び出しは、関数が適用される前にすべての引数が評価されるため、「適用順序」評価とも呼ばれます。「怠惰な呼び出し」([8のWilliam Clingerの用語を使用) ])は、厳密でないセマンティクスを使用するプロシージャ呼び出しに付けられた名前です。遅延プロシージャ呼び出しによる呼び出しがある言語では、引数はプロシージャ本体に代入される前に評価されません。遅延評価による呼び出しは、式の評価の順序(最も外側から最も内側、左から右)のため、「通常の順序」評価とも呼ばれます。「名前による呼び出し」は、ALGOLで使用される遅延による呼び出しの特定の実装です。 -60 [18]。 Algol-60の設計者は、本体を評価する前に、名前による呼び出しパラメーターを物理的に置換して、括弧で囲み、競合を避けるために適切な名前を変更することを意図していました。

レイジー対コールCALL BY NEED Call by Needは、call by lazyの拡張であり、強制された特定の遅延式の値を記憶することで遅延評価を最適化できるため、再度必要になった場合に値を再計算する必要がないという観察結果に基づいています。したがって、必要性評価による呼び出しは、メモ化を使用して遅延メソッドによる呼び出しを拡張し、繰り返し評価する必要がないようにします。フリードマンとワイズは、ニーズ評価による呼びかけの最も初期の支持者の1人であり、最初に評価されたときに自己破壊し、自分たちの価値観に取って代わった「自殺停止」を提案しました。

5
user295190

私の理解では、「非厳密」とは、より少ない作業量で完了に達するによってワークロードを削減しようとすることを意味します。

一方、「遅延評価」などは、全体的なワークロードを完全な完了を回避する(できれば永久に)削減しようとします。

あなたの例から...

f1() || f2()

...この式からの短絡は、「将来の作業」をトリガーすることにはならない可能性があり、推論に投機的/償却された要因も、計算の複雑さの負債も発生しません。

C#の例では、「lazy」は全体的なビューで関数呼び出しを保存しますが、その代わりに、上記のような問題が発生します(少なくとも呼び出しの時点から完全に完了するまで...このコードでは無視できます-距離経路ですが、これらの関数には、我慢すべきいくつかの競合の激しいロックがあったと想像してください)。

int f1() { return 1;}
int f2() { return 2;}

int lazy(int (*cb1)(), int (*cb2)() , int x) {
    if (x == 0)
        return cb1();
    else
        return cb2();
}

int eager(int e1, int e2, int x) {
    if (x == 0)
         return e1;
    else
         return e2;
}

lazy(f1, f2, x);
eager(f1(), f2(), x);
0
l.k