必要に応じて何が必要か知りたい。
ウィキペディアで検索して、http://en.wikipedia.org/wiki/Evaluation_strategyで見つけましたが、正しく理解できませんでした。誰かが例を挙げて説明し、値による呼び出しとの違いを指摘できれば、それは大きな助けになるでしょう。
必要に応じて何が必要か知りたい。
ウィキペディアで検索して、http://en.wikipedia.org/wiki/Evaluation_strategyで見つけましたが、正しく理解できませんでした。誰かが例を挙げて説明し、値による呼び出しとの違いを指摘できれば、それは大きな助けになるでしょう。
関数があるとしましょう
square(x) = x * x
そして、評価したいと思いsquare(1+2)
ます。
値による呼び出しでは、
square(1+2)
square(3)
3*3
9
名前による呼び出しでは、
square(1+2)
(1+2)*(1+2)
3*(1+2)
3*3
9
引数を2回使用するため、2回評価することに注意してください。議論の評価に時間がかかるとしたら、それはもったいないでしょう。これが、必要に応じて修正する問題です。
call-by-needでは、次のようなことを行います。
square(1+2)
let x = 1+2 in x*x
let x = 3 in x*x
3*3
9
手順2では、引数を(名前による呼び出しのように)コピーする代わりに、名前を付けます。次に、ステップ3で、の値が必要であることに気付いたときにx
、の式を評価しx
ます。そうして初めて、私たちは代用します。
ところで、引数式がクロージャなどのより複雑なものを生成した場合、let
コピーの可能性を排除するために、より多くのsのシャッフルが行われる可能性があります。正式なルールは書き留めるのがやや複雑です。
+
andのようなプリミティブ操作の引数には値が「必要」です*
が、他の関数の場合は「name、wait、see」アプローチを採用していることに注意してください。原始帰納的演算は「厳密」であると言えます。言語によって異なりますが、通常、ほとんどの基本的な操作は厳密です。
また、「評価」とは、値を減らすことを意味することにも注意してください。関数呼び出しは、式ではなく常に値を返します。(他の答えの1つがこれを間違えました。)OTOH、怠惰な言語には通常怠惰なデータコンストラクターがあり、必要に応じて、つまり抽出されたときに評価されるコンポーネントを持つことができます。このようにして、「無限の」リストを作成できます。返される値は、遅延データ構造です。ただし、call-by-needとcall-by-valueは、遅延データ構造と厳密なデータ構造とは別の問題です。Schemeには遅延データコンストラクター(ストリーム)がありますが、Schemeは値による呼び出しであるため、コンストラクターは構文形式であり、通常の関数ではありません。Haskellは名前による呼び出しですが、厳密なデータ型を定義する方法があります。
実装について考えるのに役立つ場合、名前による呼び出しの1つの実装は、すべての引数をサンクでラップすることです。引数が必要な場合は、サンクを呼び出して値を使用します。call- by- needの実装の1つは似ていますが、サンクはメモ化しています。計算を1回だけ実行し、それを保存して、その後、保存された回答を返します。
関数を想像してみてください。
fun add(a, b) {
return a + b
}
そして、それを次のように呼びます。
add(3 * 2, 4 / 2)
名前による呼び出し言語では、これは次のように評価されます。
a = 3 * 2 = 6
b = 4 / 2 = 2
return a + b = 6 + 2 = 8
関数は値を返します8
。
call-by-need(怠惰な言語とも呼ばれる)では、これは次のように評価されます。
a = 3 * 2
b = 4 / 2
return a + b = 3 * 2 + 4 / 2
関数は式を返します3 * 2 + 4 / 2
。これまでのところ、計算リソースはほとんど使用されていません。式全体は、その値が必要な場合にのみ計算されます。たとえば、結果を出力したい場合です。
なぜこれが便利なのですか?2つの理由。まず、誤ってデッドコードを含めても、プログラムの負荷がかからないため、はるかに効率的になります。次に、無限リストを使用して効率的に計算するなど、非常に優れた処理を実行できます。
fun takeFirstThree(list) {
return [list[0], list[1], list[2]]
}
takeFirstThree([0 ... infinity])
名前による呼び出し言語は、0から無限大までのリストを作成しようとしてそこにハングします。怠惰な言語は単にを返し[0,1,2]
ます。
シンプルでありながら説明的な例:
function choose(cond, arg1, arg2) {
if (cond)
do_something(arg1);
else
do_something(arg2);
}
choose(true, 7*0, 7/0);
ここで、熱心な評価戦略を使用しているとしましょう。そうすると、熱心に両方7*0
を計算し7/0
ます。それが遅延評価された戦略(必要に応じて呼び出す)である場合、それは式 7*0
を7/0
評価せずに関数に送信するだけです。
違い?do_something(0)
実際には評価戦略に依存しますが、最初の引数が使用されるため、実行することが期待されます。
言語が熱心に評価する場合、それは述べられているように、最初に評価7*0
します、そして何ですか?ゼロ除算エラー。7/0
7/0
しかし、評価戦略が怠惰な場合は、除算を計算する必要がないことがわかり、do_something(0)
エラーなしで、期待どおりに呼び出されます。
この例では、遅延評価戦略により、実行でエラーが発生するのを防ぐことができます。同様の方法で、使用しない不要な評価を実行することから実行を節約できます(ここで使用しなかったのと同じ方法で7/0
)。
これは、Cで記述されたさまざまな評価戦略の具体例です。名前による呼び出し、値による呼び出し、および必要による呼び出しの違いについて具体的に説明します。これは、ライアンの答えによって示唆されたように、前の2つ。
#include<stdio.h>
int x = 1;
int y[3]= {1, 2, 3};
int i = 0;
int k = 0;
int j = 0;
int foo(int a, int b, int c) {
i = i + 1;
// 2 for call-by-name
// 1 for call-by-value, call-by-value-result, and call-by-reference
// unsure what call-by-need will do here; will likely be 2, but could have evaluated earlier than needed
printf("a is %i\n", a);
b = 2;
// 1 for call-by-value and call-by-value-result
// 2 for call-by-reference, call-by-need, and call-by-name
printf("x is %i\n", x);
// this triggers multiple increments of k for call-by-name
j = c + c;
// we don't actually care what j is, we just don't want it to be optimized out by the compiler
printf("j is %i\n", j);
// 2 for call-by-name
// 1 for call-by-need, call-by-value, call-by-value-result, and call-by-reference
printf("k is %i\n", k);
}
int main() {
int ans = foo(y[i], x, k++);
// 2 for call-by-value-result, call-by-name, call-by-reference, and call-by-need
// 1 for call-by-value
printf("x is %i\n", x);
return 0;
}
私たちが最も興味を持っているのは、仮パラメーターの実パラメーターとしてfoo
と呼ばれるという事実です。k++
c
postfix演算子がどのように機能するかは++
、最初にk++
戻りk
、次に1ずつ増加することに注意してくださいk
。つまり、の結果k++
はちょうどk
です。(ただし、その結果が返された後は、k
1ずつ増加します。)
行(2番目のセクション)foo
までの内部のすべてのコードを無視できます。j = c + c
値による呼び出しの下でこの行に何が起こるかを次に示します。
j = c + c
値による呼び出しを行っているため、c
を評価する値がありますk++
。評価はをk++
返しk
、k
(プログラムの先頭から)0であるため、c
はになります0
。ただし、k++
1回評価したため、1に設定さk
れます。j = 0 + 0
することで、期待どおりに動作します。j
c
printf("k is %i\n", k);
と、1回k
評価したため、1になりk++
ます。名前による呼び出しの下の行で何が起こるかは次のとおりです。
c
置き換えます。したがって、線はになります。c
k++
j = (k++) + (k++)
j = (k++) + (k++)
ます。(k++)
sの1つが最初に評価され、戻って10
に設定k
されます。次に、2番目(k++)
が評価されて戻り1
(k
の最初の評価で1に設定されたためk++
)、k
2に設定されj = 0 + 1
ますk
。 2に。printf("k is %i\n", k);
と、 2回k
評価したため、 2になりk++
ます。最後に、 call-by-needの下の回線で何が起こるかを次に示します。
j = c + c;
のはこれが初めてであることがわかります。c
したがって、実際の引数を(1回)評価し、その値を格納しての評価にする必要がありc
ます。したがって、実際の引数を評価します。これは、0k++
を返すk
ため、の評価は0になります。c
次に、を評価しk++
たので、 k
1に設定されます。次に、この保存された評価を2番目のの評価として使用しますc
。つまり、名前による呼び出しとは異なり、を再評価しませんk++
。代わりに、以前に評価されたc
、0の初期値を再利用します。したがって、値渡しのj = 0 + 0;
場合と同じように取得します。c
そして、k++
一度だけ評価したので、k
は1です。j = c + c
はj = 0 + 0
必要に応じて呼び出され、期待どおりに実行されます。printf("k is %i\n", k);
と、k
評価は1回だけなので、1になりk++
ます。うまくいけば、これは、値による呼び出し、名前による呼び出し、および必要による呼び出しがどのように機能するかを区別するのに役立ちます。値による呼び出しと必要による呼び出しをより明確に区別することが役立つ場合は、コメントで知らせてください。コードの前半で、foo
そのように機能する理由を説明します。
ウィキペディアのこの行は、物事をうまくまとめていると思います。
必要による呼び出しは、名前による呼び出しのメモ化された変形であり、関数の引数が評価されると、その値は後で使用するために保存されます。引数が純粋である(つまり、副作用がない)場合、これは名前による呼び出しと同じ結果を生成し、引数を再計算するコストを節約します。