プログラミング言語において、評価戦略とは、式を評価するための一連の規則のことです。[ 1 ]この用語は、より具体的な概念であるパラメータ渡し戦略[ 2 ]を指す場合によく使われます。パラメータ渡し戦略は、各パラメータに対して関数に渡される値の種類(バインディング戦略)[ 3 ]と、関数呼び出しのパラメータを評価するかどうか、評価する場合はどのような順序で評価するか(評価順序)を定義します。[ 4 ]削減戦略の概念はこれとは異なりますが、[ 5 ]一部の著者はこれら2つの用語を混同しており、それぞれの用語の定義は広く合意されていません。[ 6 ]プログラミング言語の評価戦略は、その高レベルのセマンティクスの一部です。PureScriptなどの一部の言語には、異なる評価戦略を持つバリアントがあります。Datalogなどの一部の宣言型言語は、複数の評価戦略をサポートしています。
数学と同様に、評価とは式に対応する値を求めるプロセスである。[ 7 ] [ 8 ]
呼び出し規約は、パラメータの受け渡しに関する低レベルのプラットフォーム固有の詳細事項から構成される。
例として、関数呼び出しを実行すると、f(a, b)まず引数とを評価しa、bその結果をリファレンスまたはメモリ位置とに格納しref_a、ref_b次に渡されたこれらのリファレンスを使用して関数の本体を評価する場合があります。これにより、関数はパラメーターの逆参照によって渡された元の引数の値を検索したり(一部の言語では、これを行うために特定の演算子を使用します)、ローカル変数であるかのように代入によって値を変更したり、リファレンスを介して値を返すことができます。これが参照渡しによる評価戦略です。[ 9 ]
これは、評価戦略と代表的な言語を導入年別にまとめた表です。代表的な言語は、戦略を導入した言語から始まり、その戦略を使用している主要な言語へと時系列順にリストされています。[ 10 ]: 434
演算順序は式の抽象構文木を定義するが、評価順序は式が評価される順序を定義する。例えば、Python プログラム
def f ( x ): print ( x , end = "" ) return xprint ( f ( 1 ) + f ( 2 ))123Pythonの左から右への評価順序により出力は異なりますが、 OCamlの同様のプログラムでは次のようになります。
let 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を使用します;procedure PrintArray ( a :整数の配列) ; var i :整数; begin for i := Low ( a ) to High ( a ) do Write ( a [ i ]) ; WriteLn () ; end ;Procedure Modify ( Row :整数の配列) ; begin PrintArray ( Row ) ; // 123 Row [ 1 ] := 4 ; PrintArray ( Row ) ; // 143 end ;変数A :整数の配列; begin A := [ 1 , 2 , 3 ] ; PrintArray ( A ) ; // 123 Modify ( A ) ; PrintArray ( A ) ; // 123 end .厳密に言えば、値渡しでは、呼び出し元のルーチンによって実行される操作は、戻り値の一部として以外は、呼び出し元には見えません。[ 19 ]これは、実装セマンティクスにおいて純粋関数型プログラミングの一形態を意味します。しかし、「値が参照である値渡し」という言い回しは、Java コミュニティなど一部の言語で一般的になっています。[ 39 ]従来の値渡しと比較すると、渡される値は、リテラルとして記述できる整数など、通常の意味での値ではなく、実装内部の参照ハンドルです。この参照ハンドルの変更は、呼び出し元で見えます。変更が見えることから、この形式の「値渡し」は、共有による呼び出しとより適切に呼ばれます。[ 19 ]
純粋関数型言語では、値とデータ構造は不変であるため、関数が引数を変更することは不可能です。そのため、値渡しと参照渡し、あるいはデータ構造へのポインタ渡しの間には、通常、意味的な違いはありません。また、実装においては、効率性の観点から、内部的に参照渡しが頻繁に用いられます。とはいえ、これらの言語は一般的に値渡し言語と呼ばれています。
参照渡し(または参照による呼び出し)は、引数として使用される変数の値のコピーではなく、その変数への暗黙的な参照にパラメータがバインドされる評価戦略です。これは通常、関数が引数として使用される変数を変更(つまり、代入)できることを意味し、呼び出し元はその変更を確認できます。したがって、参照渡しは、呼び出される関数と呼び出し元の関数の間に追加の通信チャネルを提供するために使用できます。参照による呼び出しは、パフォーマンスを大幅に向上させることができます。数メガバイトの構造体を引数として関数を呼び出す場合、大きな構造体をコピーする必要はなく、構造体への参照(通常はマシンワードで、わずか数バイト)だけをコピーすればよいのです。ただし、参照渡し言語では、プログラマが関数呼び出しの影響を追跡するのが難しくなり、微妙なバグが発生する可能性があります。
構文のバリエーションがあるため、参照による呼び出し(参照型が暗黙的)と共有による呼び出し(参照型が明示的)の違いは、一見すると不明瞭な場合が多い。簡単な判断基準は、swap(a, b)その言語で従来の関数を記述できるかどうかである。[ 39 ]例えば、Fortranでは次のようになる。
program Main implicit none integer :: a = 1 integer :: b = 2 call Swap ( a , b ) print * , a , b ! 2 1 contains subroutine Swap ( a , b ) integer , intent ( inout ) :: a , b integer :: temp temp = a a = b b = temp end subroutine Swap end program Mainしたがって、Fortranのinout意図は参照渡しを実装しており、任意の変数は暗黙的に参照ハンドルに変換できます。これに対し、Javaで最も近いものは次のようになります。
public class Main { static class Box { int value ; public Box ( int value ) { 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の動作は戻り時に左から右の順序でコピーすることであることがわかります。
with Ada.Text_IO ; use Ada.Text_IO ;procedure Test_Copy_Restore is procedure Modify ( A , B : in out Integer ) is 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: warning: writable actual for "A" overlaps with actual for "B" [-gnatw.i] -- X = 2プログラムが1を返した場合、それは右から左へのコピーであり、参照渡しセマンティクスではプログラムは3を返します。
参照が初期化されていない状態で呼び出し元に渡される場合(例えば、outAda のパラメータではなく、通常のin outパラメータの場合)、この評価戦略は「結果による呼び出し」と呼ばれることがあります。
この戦略は、参照渡しとは異なり、変数へのアクセスに実行スレッド間の頻繁な通信を必要としないため、マルチプロセッシングやリモートプロシージャコールで注目を集めています[ 44 ] 。
呼び出し共有(「パス共有」、「オブジェクトによる呼び出し」、または「オブジェクト共有による呼び出し」とも呼ばれる)は、値による呼び出しと参照による呼び出しの中間の評価戦略です。すべての変数が参照として公開されるのではなく、「参照」、「ボックス型」、または「オブジェクト」と呼ばれる特定のクラスの値のみが参照セマンティクスを持ち、これらのポインタのアドレスが関数に渡されます。値による呼び出しと同様に、渡されたアドレスの値はコピーであり、関数のパラメータへの直接代入はコピーを上書きし、呼び出し元の関数からは見えません。参照による呼び出しと同様に、ポインタのターゲットを変更すると、呼び出し元の関数から見えます。関数内の可変オブジェクトの変更は、オブジェクトがコピーまたはクローンされるのではなく共有されるため、呼び出し元から見えます。これが「呼び出し共有」という名前の由来です。[ 19 ]
この手法は、1974 年にBarbara Liskovが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 : list [ int ] = [] f ( m ) print ( m )メソッドが呼び出されたオブジェクトを変更する[1]ため、出力が生成されます。append
対照的に、関数内の代入は呼び出し元には見えません。たとえば、このコードは仮引数を新しいオブジェクトにバインドしますが、オブジェクトを変更しないため、呼び出し元には見えませんa_list。
def f ( l : list [ int ]) -> None : l = l + [ 1 ] print ( l ) # [1]m : list [ int ] = [] f ( m ) print ( 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 return 0 ; }一部の著者は、&を呼び出しの構文の一部として扱いますswap。この見解では、C は参照渡しパラメータ戦略をサポートしています。[ 50 ]swap他の著者は、C で提示されたの実装はポインタを使用した参照渡しのシミュレーションにすぎないという異なる見解をとります。 [ 51 ]この「シミュレーション」の見解では、C の可変変数は第一級ではありません (つまり、l 値は式ではありません)、むしろポインタ型が第一級です。この見解では、提示された swap プログラムは、全体を通してポインタを使用するプログラムの構文糖衣です。 [ 52 ]例えば、このプログラム (と はread、上記のassignJava のBox共有渡しプログラムとの類似性を強調するために追加されています)。
#include <stdio.h>int read ( int * p ) { return * p ; }void assign ( 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言語の主な評価戦略は共有呼び出しにより近いものとなっている。
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 return 0 ; }意味的には、これはC言語の例と同等です。そのため、多くの著者は、アドレス渡しを値渡し、参照渡し、共有渡しとは異なる独自のパラメータ渡し戦略であると考えています。
論理プログラミングにおいて、式の評価は、関係する項の単一化と何らかの形式の分解の適用に単純に対応します。単一化は完全に実行されるため、厳密な束縛戦略として分類されます。ただし、単一化は無制限の変数に対しても実行できるため、呼び出しによってすべての変数の最終値が必ずしも確定されるとは限りません。
名前による呼び出しとは、関数の引数を関数呼び出し前に評価しない評価戦略です。引数は(キャプチャ回避置換を用いて)関数本体に直接代入され、関数内で出現するたびに評価されます。関数本体で引数が使用されない場合は、その引数は評価されません。複数回使用される場合は、出現するたびに再評価されます。(この仕組みを利用したプログラミング手法については、ジェンセンの装置を参照してください。)
名前による呼び出し評価は、値による呼び出し評価よりも場合によっては好ましいことがあります。関数の引数が関数内で使用されない場合、名前による呼び出しでは引数を評価しないため、時間を節約できます。一方、値による呼び出しでは引数が必ず評価されます。引数が非終了型の計算である場合、その利点は非常に大きくなります。ただし、関数の引数が使用される場合は、名前による呼び出しはしばしば遅くなるため、サンクなどのメカニズムが必要になります。
.NET言語では、デリゲートまたはExpression<T>パラメーターを使用して名前による呼び出しをシミュレートできます。後者の場合、関数に抽象構文木が渡されます。Eiffelは、必要に応じて評価される操作を表すエージェントを提供します。Javaプログラムでも、ラムダ式とインターフェースを使用して同様の遅延評価を実現できますjava.util.function.Supplier<T>。
必要に応じて呼び出す方式は、名前呼び出しのメモ化されたバリアントであり、関数引数が評価された場合、その値が後続の使用のために保存されます。引数が純粋(つまり、副作用がない)である場合、これは名前呼び出しと同じ結果を生み出し、引数を再計算するコストを削減します。
Haskellは、必要に応じて値を変更する「コールバイニーズ評価」を採用した有名なプログラミング言語です。式の評価は計算過程のどの段階でも発生する可能性があるため、Haskellはモナドを用いることで副作用(例えば、ミューテーション)のみをサポートしています。これにより、遅延評価前に値が変化する変数による予期せぬ動作を防ぐことができます。
Rにおける「必要に応じて呼び出す」の実装では、すべての引数が渡されるため、Rは任意の副作用を許容します。
遅延評価は、必要に応じて呼び出すセマンティクスの最も一般的な実装ですが、楽観的評価などのバリエーションも存在します。.NET言語は、型を使用して必要に応じて呼び出しを実装しますLazy<T>。
グラフ縮小は、遅延評価の効率的な実装方法である。
マクロ展開による呼び出しは名前による呼び出しと似ていますが、キャプチャ回避置換ではなくテキスト置換を使用します。そのため、マクロ置換では変数キャプチャが発生し、エラーや意図しない動作につながる可能性があります。衛生的なマクロは、パラメーターではないシャドウ変数をチェックして置換することで、この問題を回避します。
「未来による呼び出し」は、「並列名前呼び出し」または「寛容な評価」とも呼ばれ、[ 54 ]は、厳密でない意味論と積極的な評価を組み合わせた並行評価戦略です。この方法は、きめ細かい動的スケジューリングと同期を必要としますが、大規模並列マシンに適しています。
この戦略では、関数の本体と各引数に対して未来(プロミス)を作成します。これらの未来は、プログラムの残りのフローと並行して計算されます。未来 A がまだ計算されていない別の未来 B の値を必要とする場合、未来 A は未来 B の計算が完了して値が得られるまでブロックします。未来 B の計算がすでに完了している場合は、値がすぐに返されます。条件式は条件が評価されるまでブロックし、ラムダ式は完全に適用されるまで未来を作成しません。[ 55 ]
プロセスまたはスレッドを使用して実装する場合、Future を作成すると、1 つ以上の新しいプロセスまたはスレッド (プロミス用) が生成され、値にアクセスすると、これらがメインスレッドと同期され、Future の計算を終了すると、その値を計算するプロミスが破棄されます。.NETのasync/awaitのようにコルーチンを使用して実装する場合、Future の作成はコルーチン (非同期関数) を呼び出し、呼び出し元に処理を委譲し、値が使用されるときに呼び出し元に処理を委譲することで、協調的なマルチタスク処理が行われます。
この戦略は非決定論的であり、評価は未来の生成(つまり、式が与えられたとき)から未来の値の使用までの間の任意の時点で発生する可能性があります。この戦略は厳密ではなく、関数本体が引数の評価前に値を返す可能性があります。ただし、ほとんどの実装では、不要な引数の評価で実行が停止する可能性があります。たとえば、プログラム
f x = 1 / x g y = 1 main = print ( g ( f 0 ))は、gより前に終了しfて 1 を出力するか、または の評価によりエラーになる可能性がある1/0。[ 31 ]
先物による呼び出しは、値が一度だけ計算されるという点で、必要による呼び出しと似ています。エラーや非終了を注意深く処理し、特に必要ないと判断された場合は途中で先物を終了させることで、先物による呼び出しは必要による評価と同じ終了特性を持ちます。[ 55 ]ただし、先物による呼び出しは、遅延データ構造を深く評価するなど、必要による呼び出しと比較して不要な投機的作業を実行する可能性があります。[ 31 ]これは、値が必要であることが確実になるまで計算を開始しない遅延先物を使用することで回避できます。
楽観的評価は、関数の引数を一定時間(実行時に調整可能)値渡し方式で部分的に評価する、呼び出しバイニーズのバリアントです。その時間が経過すると、評価は中止され、呼び出しバイニーズを使用して関数が適用されます。[ 56 ]このアプローチは、望ましい終了特性を維持しながら、呼び出しバイニーズの実行時コストの一部を回避します。
数学。(量的表現の)「値」を計算する。(量的事実または関係の)数値表現を見つける。
(方程式やその他の数式を)理解、分析、または扱いやすい形式で表現すること。例えば、同類項をまとめたり、変数を代入したりすることによって。
おそらく、遅延評価の力を体系的に活用した最初の言語だったでしょう。
{{cite web}}: CS1メンテナンス: アーカイブサービスは非推奨になりました (リンク)