| 評価戦略 |
|---|
プログラミング言語において、評価戦略とは式を評価するための一連の規則のことである。[ 1 ]この用語は多くの場合、パラメータ渡し戦略[ 2 ]というより具体的な概念を指すのに使用され、これは関数の各パラメータに対して渡される値の種類 (バインディング戦略) [ 3 ]と、関数呼び出しのパラメータを評価するかどうか、評価する場合はどの順序で評価するか (評価順序) を定義する。[ 4 ]削減戦略という概念は異なるが[ 5 ]、一部の著者は 2 つの用語を混同しており、それぞれの用語の定義は広く合意されているわけではない。[ 6 ]プログラミング言語の評価戦略は、その高水準意味論の一部である。 PureScriptなどの一部の言語には、異なる評価戦略を持つ変種がある。Datalogなどの一部の宣言型言語は、複数の評価戦略をサポートしている。
数学と同様に、評価とは式に対応する値を見つけるプロセスです。[ 7 ] [ 8 ]
呼び出し規約は、パラメータ渡しの低レベルのプラットフォーム固有の詳細で構成されます。
例えば、関数呼び出しを実行すると、f(a, b)まず引数とを評価しa、bその結果を参照またはメモリ位置とに格納しref_a、ref_b渡された参照を使って関数本体を評価する。これにより、関数はパラメータの逆参照(一部の言語では特定の演算子を使用してこれを行う)によって渡された元の引数値を参照し、それらをローカル変数のように代入によって変更し、参照を介して値を返すことができる。これが参照渡し評価戦略である。[ 9 ]
これは、評価戦略と代表的な言語を導入年ごとにまとめた表です。代表的な言語は、導入された言語から始まり、その戦略を採用している主要な言語へと、年代順に並べられています。[ 10 ] : 434
| 評価戦略 | 代表的な言語 | 初導入年 |
|---|---|---|
| 参照による呼び出し | Fortran II、PL/I | 1958 |
| 値による呼び出し | ALGOL、C、[ 11 ] Scheme、MATLAB [ 12 ] | 1960 |
| 名前を呼ぶ | ALGOL 60、Simula | 1960 |
| コピー復元による呼び出し | Fortran IV、Ada [ 13 ] | 1962 |
| 統一による呼び出し | プロローグ | 1965年[ 14 ] [ 15 ] |
| 必要に応じて呼び出し | SASL、[ 16 ]ハスケル、R [ 17 ] | 1971年[ 18 ] |
| 共有して通話 | CLU、Java、Python、Ruby、Julia | 1974年[ 19 ] |
| 参照パラメータによる呼び出し | C++、PHP、[ 20 ] C#、[ 21 ] Visual Basic .NET [ 22 ] | 1985年[ 23 ] |
| constへの参照呼び出し | C++、C | 1985年[ 23 ] |
演算の順序は式の抽象構文木を定義しますが、評価順序は式が評価される順序を定義します。例えば、Pythonプログラム
def f ( x ): print ( x , end = "" ) return x印刷( f ( 1 ) + f ( 2 ) )123Python の左から右への評価順序により出力は異なりますが、 OCamlでは同様のプログラムは次のようになります。
f x = print_int x ; x ;; print_int ( f 1 + f 2 )とします。213OCaml の右から左への評価順序による 出力です。
評価順序は主に副作用のあるコードで顕著ですが、厳格な順序は命令スケジューリングを阻害するため、コードのパフォーマンスにも影響します。このため、C++などの言語標準では伝統的に順序が明示されていませんでしたが、JavaやC#などの言語では評価順序が左から右と定義されており[ 10 ] : 240–241 、C++17標準では評価順序に制約が追加されました[ 24 ] 。
適用順序は、関数の引数が関数が適用される前に完全に評価される評価順序の一種である。 [ 25 ]これは関数を厳密なものにする効果があり、つまり引数のいずれかが未定義の場合、関数の結果は未定義となるため、適用順序評価はより一般的には厳密評価と呼ばれる。さらに、関数呼び出しは手続き内で遭遇するとすぐに実行されるため、先行評価または貪欲評価とも呼ばれる。[ 26 ] [ 27 ]値渡しバインディング戦略が厳密な評価を必要とするため、一部の著者は厳密な評価を「値渡し」と呼ぶ。[ 4 ]
Common Lisp、Eiffel、Javaでは関数の引数は左から右に評価されます。Cでは順序は未定義です。[ 28 ] Schemeでは実行順序は引数の不特定の順列を順に実行する必要があります。[ 29 ] OCamlも同様に順序は未定義ですが、抽象マシンの設計により、実際には引数は右から左に評価されます。[ 30 ]これらはすべて厳密な評価です。
非厳密な評価順序とは、厳密ではない評価順序のことです。つまり、関数はすべての引数が完全に評価される前に結果を返すことがあります。[ 31 ] : 46–47 典型的な例は、関数本体で引数が必要になるまで引数を評価しない通常順序評価です。 [ 32 ]通常順序評価は、他の評価順序であればエラーなく終了するのと同じ場合に、エラーなく終了するという性質があります。[ 33 ]「通常順序」という名称は、ラムダ計算に由来しています。ラムダ計算では、通常順序の縮約は、正規形が存在する場合はそれを見つけます(これは「正規化」縮約戦略です)。[ 34 ]遅延評価は、この記事では評価順序ではなく、束縛手法として分類されています。しかし、この区別は常に守られるわけではなく、著者によっては遅延評価を通常順序評価と定義したり、その逆を行ったりする人もいます。[ 25 ] [ 35 ]また、非厳密性と遅延評価を混同する人もいます。[ 31 ] : 43–44
多くの言語のブール式では、短絡評価と呼ばれる非厳密な評価形式が採用されています。短絡評価では、左側の式を評価しますが、結果が決定可能な場合は右側の式をスキップすることがあります。たとえば、論理和式 (OR) でtrueが出現する場合や、論理積式 (AND) で がfalse出現する場合などです。[ 35 ]条件式でも同様に非厳密な評価が採用されており、分岐の1つだけが評価されます。[ 31 ]
通常順序評価では、高価な計算、エラー、または無限ループを含む式は、必要でない場合は無視されます。[ 4 ]これにより、ユーザー定義の制御フロー構造の指定が可能になります。これは、適用順序評価では利用できない機能です。通常順序評価では、適用順序評価で使用されるコールスタックと比較して、評価されていない式にサンクなどの複雑な構造が使用されます。[ 36 ]通常順序評価は、その複雑さのために、歴史的に使用可能なデバッグツールが不足していました。[ 37 ]
値渡し(または値渡し)では、引数式の評価値は関数内の対応する変数にバインドされます(多くの場合、値は新しいメモリ領域にコピーされます)。関数または手続きがパラメータに値を代入できる場合、ローカル変数のみが割り当てられます。つまり、関数呼び出しに渡された値は、関数が返されたときに呼び出し元のスコープ内で変更されません。例えば、Pascalでは、配列を値渡しすると、配列全体がコピーされ、この配列に対する変更は呼び出し元には見えません。[ 38 ]
プログラムMain ; crtを使用します;手順PrintArray ( a :整数の配列) ; var i : Integer ; begin for i := Low ( a ) to High ( a ) do Write ( a [ i ] ) ; WriteLn ( ) ; end ;手順Modify ( Row :整数の配列) ; begin PrintArray ( Row ) ; // 123 Row [ 1 ] := 4 ; PrintArray ( Row ) ; // 143 end ;Var A :整数の配列; begin A : = [ 1,2,3 ] ; PrintArray ( A ) ; // 123 Modify ( A ) ; PrintArray ( A ) ; // 123 end .厳密に言えば、値渡しでは、呼び出されたルーチンによって実行される操作は、戻り値の一部としてのみ呼び出し元から見える。[ 19 ]これは、実装セマンティクスにおいて純粋関数型プログラミングの一種であることを意味する。しかし、「値が参照である場合の値渡し」という回りくどい表現は、Javaコミュニティなど一部の言語で一般的になっている。[ 39 ]従来の値渡しと比較すると、渡される値は、リテラルとして記述できる整数のような、通常の意味での値ではなく、実装内部の参照ハンドルである。この参照ハンドルの変化は呼び出し元から見える。変化が見えることから、この形式の「値渡し」は、より正確には共有による呼び出しと呼ばれる。[ 19 ]
純粋関数型言語では、値とデータ構造は不変であるため、関数が引数を変更する可能性はありません。そのため、値渡しと参照渡し、あるいはデータ構造へのポインタ渡しの間には通常、意味的な違いはなく、実装では効率性の向上のために内部的に参照渡しが頻繁に使用されます。それでもなお、これらの言語は一般的に値渡し言語と呼ばれます。
参照呼び出し(または参照渡し)は、パラメータが、引数として使用される変数の値のコピーではなく、その変数への暗黙的な参照にバインドされる評価戦略です。 これは通常、関数が引数として使用される変数を変更(つまり、割り当て)できることを意味し、これは呼び出し元に表示されます。 したがって、参照呼び出しを使用すると、呼び出される関数と呼び出し元の関数の間に追加の通信チャネルを提供できます。 参照渡しにより、パフォーマンスが大幅に向上します。 数メガバイトの構造体を引数として関数を呼び出す場合、大きな構造体をコピーする必要がなく、構造体への参照(通常はマシンワードで数バイトのみ)のみをコピーすればよいことになります。 ただし、参照呼び出し言語では、プログラマが関数呼び出しの効果を追跡することが難しくなり、微妙なバグが発生する可能性があります。
構文の多様性により、参照呼び出し(参照型が暗黙的)と共有呼び出し(参照型が明示的)の違いは、一見すると分かりにくいことがよくあります。簡単なリトマス試験は、swap(a, b)その言語で従来の関数を記述できるかどうかです。[ 39 ]例えばFortranでは:
プログラムメイン暗黙的 なし整数:: a = 1整数:: b = 2呼び出しSwap ( a , b )印刷* , a , b ! 2 1サブルーチンSwap ( a , b ) 整数、インテント( inout ) :: a , b整数:: temp temp = a a = b b = tempサブルーチン終了Swap終了プログラムメインしたがって、Fortranはinout参照渡しを実装することを意図しており、あらゆる変数は暗黙的に参照ハンドルに変換されます。対照的に、Javaで最も近いものは次のとおりです。
パブリッククラスMain {静的クラスBox { int値;パブリックBox ( int値) { this . value = value ; } }static void swap ( Box a 、Box b ) { int temp = a . value ; a . value = b . value ; b . value = temp ; }public static void main ( String [] args ) { Box a = new Box ( 1 ) ; Box b = new Box ( 2 ) ; swap ( a , b ); System.out.printf ( " a = % d, b = %d% n " , a.value , b.value ) ; // 出力: a = 2, b = 1 } }ハンドルを導入するには明示的なBox型を使用する必要があります。Javaは共有呼び出しですが、参照呼び出しではありません。[ 39 ]
コピー・リストアによる呼び出し( Fortranコミュニティでは「コピーイン・コピーアウト」、「値渡し結果」、「値渡し戻り値」とも呼ばれる)は、参照呼び出しの一種です。コピー・リストアによる呼び出しでは、引数の内容は呼び出し呼び出しのローカルな新しい変数にコピーされます。関数はその後、参照呼び出しと同様にこの変数を変更することができますが、変数がローカルであるため、呼び出し中は呼び出し呼び出しの外部から変更内容は参照できません。関数呼び出しが戻ると、この変数の更新された内容がコピーされ、元の引数が上書きされます(「復元」されます)。[ 40 ]
コピー・リストアによる呼び出しの意味は、多くの場合参照による呼び出しと似ていますが、2つ以上の関数引数が互いにエイリアスになっている場合(つまり、呼び出し元の環境で同じ変数を指している場合)は異なります。参照による呼び出しでは、関数の実行中に一方の引数に書き込むと、もう一方の引数にも影響します。コピー・リストアによる呼び出しでは、関数の実行中に一方の引数に書き込んでももう一方の引数には影響しませんが、呼び出しの終了時に2つの引数の値が異なる場合があり、どちらの引数が最初にコピーバックされるか、つまり呼び出し元の変数がどの値を受け取るかは不明です。[ 41 ]in out例えば、Adaでは、各またはパラメータのコピーアウト代入はout任意の順序で行われるように指定されています。[ 42 ]次のプログラム(Ada 2012では不正)[ 43 ]から、 GNATの動作は戻り時に左から右の順序でコピーすることであることがわかります。
Ada.Text_IOで; Ada.Text_IOを使用します;手順Test_Copy_Restoreは、手順Modify ( A , B : in out Integer )で、begin A := A + 1 ; B := B + 2 ; end Modify ; X : Integer := 0 ; begin Modify ( X , X ); Put_Line ( "X = " & Integer ' Image ( X )); end Test_Copy_Restore ; -- $ gnatmake -gnatd.E test_copy_restore.adb; ./test_copy_restore -- test_copy_restore.adb:12:10: 警告: "A" の書き込み可能な実際の値が "B" の実際の値と重複しています [-gnatw.i] -- X = 2プログラムが 1 を返す場合、右から左へのコピーとなり、参照による呼び出しセマンティクスではプログラムは 3 を返します。
参照が初期化されていない状態で呼び出し元に渡される場合 (たとえば、パラメータoutではなく Ada のパラメータin out)、この評価戦略は「結果による呼び出し」と呼ばれることがあります。
この戦略は、参照呼び出しとは異なり、変数アクセスのために実行スレッド間で頻繁な通信を必要としないため、 マルチプロセスおよびリモートプロシージャコールで注目を集めています。[ 44 ]
共有呼び出し(「共有渡し」、「オブジェクト呼び出し」、「オブジェクト共有呼び出し」とも呼ばれる)は、値呼び出しと参照呼び出しの中間的な評価戦略です。すべての変数が参照として公開されるのではなく、「参照」、「ボックス化型」、または「オブジェクト」と呼ばれる特定の値クラスのみが参照セマンティクスを持ち、これらのポインタのアドレスが関数に渡されます。値呼び出しと同様に、渡されるアドレスの値はコピーであり、関数のパラメータに直接代入するとコピーが上書きされ、呼び出し元の関数からは参照できません。参照呼び出しと同様に、ポインタのターゲットの変更は呼び出し元の関数から参照できます。関数内の可変オブジェクトの変更は、オブジェクトがコピーまたはクローン化されていない(つまり共有されている)ため、呼び出し元から参照できます。そのため、「共有呼び出し」と呼ばれます。[ 19 ]
この手法は、1974年にバーバラ・リスコフによってCLU言語で初めて言及されました。[ 19 ]これは、Python(共有値は「オブジェクト」と呼ばれます)、[ 45 ] Java(オブジェクト)、Ruby(オブジェクト)、JavaScript(オブジェクト)、Scheme(ベクターなどのデータ構造)、[ 46 ] AppleScript(リスト、レコード、日付、スクリプトオブジェクト)、OCamlとML(参照、レコード、配列、オブジェクト、その他の複合データ型)、Maple(rtableとテーブル)、Tcl(オブジェクト)など、多くの現代言語で使用されています。[ 47 ]この記事で使用されている「共有による呼び出し」という用語は一般的には使用されていません。この用語はさまざまな情報源間で一貫性がありません。例えば、Javaコミュニティでは、Javaは値呼び出しであると言われています。[ 39 ]
不変オブジェクトの場合、オブジェクトの同一性が言語内で可視である場合を除き、共有呼び出しと値呼び出しの間に実質的な違いはありません。可変オブジェクトにおける共有呼び出しの使用は、入出力パラメータの代替手段です。パラメータは代入されず(引数は上書きされず、オブジェクトの同一性も変更されません)、オブジェクト(引数)は変更されます。[ 48 ]
たとえば、Python ではリストは変更可能であり、共有によって呼び出しで渡されるので、次のようになります。
def f ( l : list [ int ]) -> None : l . append ( 1 )m :リスト[ int ] = [] f ( m )プリント( m )メソッドは、呼び出されたオブジェクトを変更する [1]ため、出力されます。append
対照的に、関数内での代入は呼び出し元には見えません。例えば、次のコードは仮引数を新しいオブジェクトにバインドしますが、これは変更されないため、呼び出し元には見えませんa_list。
def f ( l : list [ int ]) -> None : l = l + [ 1 ] print ( l ) # [1]m :リスト[ int ] = [] f ( m )プリント( m ) # []アドレス呼び出し、アドレス渡し、またはポインタ呼び出し/渡しは、引数のアドレスを仮引数として渡す引数渡し方法です。関数内では、アドレス(ポインタ)を使って引数の値にアクセスしたり、変更したりすることができます。例えば、スワップ操作はC言語で次のように実装できます。[ 49 ]
#include <stdio.h>void swap ( int * a , int * b ) { int temp = * a ; * a = * b ; * b = temp ; }int main () { int a = 1 ; int b = 2 ; swap ( &a a , & b ); printf ( "%d %d \n " , a , b ); // 2 1 0を返す; }一部の著者は、&を 呼び出しの構文の一部として扱っていますswap。この見解では、C は参照渡しのパラメータ渡し戦略をサポートしています。[ 50 ]他の著者は、C での の実装は、swapポインタを使用した参照渡しのシミュレーションにすぎないという異なる見解を持っています。[ 51 ]この「シミュレーション」の見解では、C の可変変数はファーストクラスではなく (つまり、左辺値は式ではありません)、ポインタ型がファーストクラスです。この見解では、提示された swap プログラムは、ポインタを全体を通して使用するプログラムの糖衣構文です。 [ 52 ]例えば、次のプログラムです (およびreadは、上記のassignJavaBoxの共有渡しプログラムとの類似性を強調するために追加されています)。
#include <stdio.h>int読み取り( int * p ) {戻り値* p ; }void代入( int * p , int v ) { * p = v ; }void swap ( int * a , int * b ) { int temp_storage ; int * temp = & temp_storage ; assign ( temp , read ( a )); assign ( a , read ( b )); assign ( b , read ( temp )); }int main () { int a_storage ; int * a = & a_storage ; int b_storage ; int * b = & b_storage ; assign ( a , 1 ); assign ( b , 2 ); swap ( a , b ); printf ( "%d %d \n " , read ( a ), read ( b )); // 2 1 return 0 ; }このプログラムでは、swapポインターを操作し、ポインター自体を変更することはできず、ポインターが指す値のみを変更するため、この見解では、C の主な評価戦略は call-by-sharing に似ていると考えられます。
swapC++では、非常に軽量な「参照」構文で宣言および使用することを許可することで、問題をさらに複雑にしています。[ 53 ]
void swap ( int & a , int & b ) { int temp = a ; a = b ; b = temp ; }int main () { int a = 1 ; int b = 2 ; swap ( a , b ); std :: println ( "{} {}" , a , b ); // 2 1 0を返す; }意味的には、これはC言語の例と同等です。そのため、多くの著者は、アドレス呼び出しを、値呼び出し、参照呼び出し、共有呼び出しとは異なる独自のパラメータ渡し戦略であると考えています。
論理プログラミングにおいて、式の評価は、単に関係する項の単一化と何らかの解決法の適用を組み合わせたものに相当する場合があります。単一化は完全に実行されるため、厳密な束縛戦略に分類されます。ただし、単一化は無制限の変数に対しても実行されるため、呼び出しによってすべての変数の最終的な値が必ずしも確定されるとは限りません。
名前呼び出しは、関数の引数を関数が呼び出される前に評価しない評価戦略です。引数は関数本体に直接代入され(キャプチャ回避置換を使用)、関数内で出現するたびに評価されます。引数が関数本体で使用されない場合は評価されませんが、複数回使用される場合は、出現するたびに再評価されます。(これを利用したプログラミング手法については、Jensenのデバイスを参照してください。)
名前呼び出しによる評価は、値呼び出しによる評価よりも好ましい場合があります。関数の引数が関数内で使用されていない場合、名前呼び出しは引数を評価しないため時間を節約できますが、値呼び出しは引数を評価せずにそのまま評価します。引数が終了しない計算である場合、この利点は非常に大きくなります。しかし、関数の引数が使用される場合、名前呼び出しは多くの場合速度が遅くなり、サンクなどのメカニズムが必要になります。
.NET言語では、デリゲートまたはExpression<T>パラメータを用いて名前による呼び出しをシミュレートできます。後者の場合、関数に抽象構文木が与えられます。Eiffelは、必要に応じて評価される操作を表すエージェントを提供します。Javaプログラムでは、ラムダ式とインターフェースを用いて同様の遅延評価を実現できますjava.util.function.Supplier<T>。
call by need はcall by name のメモ化された変種であり、関数の引数が評価された場合、その値が後で使用するために保存されます。引数が純粋引数(つまり副作用がない引数)の場合、call by name と同じ結果が生成され、引数の再計算コストを節約できます。
Haskellはcall-by-need評価を採用する有名な言語です。式の評価は計算の任意の時点で行われる可能性があるため、Haskellはモナドを用いることで副作用(突然変異など)のみをサポートしています。これにより、遅延評価前に値が変化する変数による予期せぬ動作が排除されます。
Rの call by need の実装では、すべての引数が渡されます。つまり、 R では任意の副作用が許容されます。
遅延評価は call-by-need セマンティクスの最も一般的な実装ですが、楽観的評価などのバリエーションも存在します。.NET言語は、型を使用して call by need を実装しますLazy<T>。
グラフ削減は遅延評価の効率的な実装です。
マクロ展開による呼び出しは名前による呼び出しに似ていますが、キャプチャ回避置換ではなくテキスト置換を使用します。そのため、マクロ置換は変数キャプチャを引き起こし、ミスや望ましくない動作につながる可能性があります。衛生的なマクロは、パラメータではない シャドウ変数をチェックして置換することで、この問題を回避します。
「call by future」は、「parallel call by name」または「lenient evaluation」とも呼ばれ、[ 54 ]非厳密なセマンティクスと先行評価を組み合わせた並行評価戦略です。この手法は、細粒度の動的スケジューリングと同期を必要としますが、超並列マシンに適しています。
この戦略は、関数本体とその各引数に対してFuture(Promise)を作成します。これらのFutureは、プログラムの残りのフローと並行して計算されます。Future Aがまだ計算されていない別のFuture Bの値を必要とする場合、Future AはFuture Bの計算が終了して値を取得するまでブロックします。Future Bの計算がすでに終了している場合は、その値が直ちに返されます。条件文は条件が評価されるまでブロックし、ラムダ式は完全に適用されるまでFutureを作成しません。[ 55 ]
プロセスまたはスレッドで実装されている場合、Future の作成時に 1 つ以上の新しいプロセスまたはスレッド(Promise 用)が生成され、値にアクセスするとこれらのプロセスまたはスレッドがメインスレッドと同期されます。Future の計算を終了すると、その値を計算している Promise が強制終了されます。.NET の async/await のように coroutine で実装されている場合、Futureの作成時に coroutine(非同期関数)が呼び出されます。coroutine は呼び出し元に処理を譲り渡し、値が使用されると coroutine に処理を譲り渡すため、協調的なマルチタスクが実行されます。
この戦略は非決定的であり、評価はFutureの生成(つまり式が与えられた時点)からFutureの値の使用までの間、いつでも発生する可能性があります。また、関数本体が引数が評価される前に値を返す可能性があるため、この戦略は非厳密です。しかし、ほとんどの実装では、不要な引数の評価で実行が停止する可能性があります。例えば、以下のプログラムは
f x = 1 / x g y = 1 main = print ( g ( f 0 ))はgの前に終了しfて 1 を出力するか、 を評価することでエラーが発生する可能性があります1/0。[ 31 ]
call-by-futureは、値が一度だけ計算されるという点でcall-by-needに似ています。エラーや非終了性を慎重に処理し、特にfutureが不要と判断された場合は途中で終了させることで、call-by-futureはcall-by-needの評価と同じ終了特性を持ちます。[ 55 ]しかし、call-by-futureはcall-by-needと比較して、遅延データ構造の深い評価など、不要な投機的な作業を行う可能性があります。[ 31 ]これは、値が必要であることが確実になるまで計算を開始しない遅延futureを使用することで回避できます。
楽観的評価はcall-by-needの派生形であり、関数の引数は一定時間(実行時に調整可能)にわたってcall-by-value形式で部分的に評価されます。その時間が経過すると評価は中止され、関数はcall-by-needを使用して適用されます。[ 56 ]このアプローチは、call-by-needによる実行時のコストをある程度回避しながら、望ましい終了特性を維持します。
数学。(定量的な表現)の「値」を求めること。(定量的な事実や関係)の数値表現を求めること。
(方程式やその他の数式を)理解、分析、または操作しやすい形で表現すること。例えば、同類項をまとめたり、変数を置き換えたりすることなど。
おそらく遅延評価の威力を体系的に活用した最初の言語です。