コンピュータプログラミングにおいて、関数(プロシージャ、メソッド、サブルーチン、ルーチン、またはサブプログラムとも呼ばれる)は、整ったインターフェースと動作を持ち、複数回呼び出すことができるソフトウェアロジックの呼び出し可能な単位[ 1 ]である。
呼び出し可能なユニットは強力なプログラミングツールを提供します。[ 2 ]主な目的は、大規模または複雑な問題を、認知負荷が比較的低いチャンクに分解し、チャンクに意味のある名前を割り当てることを可能にすることです(匿名でない場合)。適切に適用することで、ソフトウェアの開発と保守のコストを削減し、品質と信頼性を向上させることができます。[ 3 ]
プログラミング環境では、呼び出し可能なユニットが複数の抽象化レベルに存在します。 [ 4 ]例えば、プログラマーは、同様のセマンティクスを実装するマシンコードにコンパイルされるソースコードに関数を記述することがあります。ソースコードには呼び出し可能なユニットがあり、マシンコードにもそれに関連付けられた呼び出し可能なユニットがありますが、これらは異なる種類の呼び出し可能なユニットであり、意味や機能も異なります。
COBOLやBASICなどのプログラミング言語では、値を返す関数(一般的に「関数」と呼ばれる)と返さない関数(一般的に「サブプログラム」、「サブルーチン」、「プロシージャ」と呼ばれる)を区別しています。C、C++、Rustなどの言語では、値を返すかどうかに関わらず「関数」という用語のみを使用します。ALGOL 60やPL/Iなどの言語では、 「プロシージャ」という用語のみを使用します。JavaやC#などのオブジェクト指向言語では、クラス内の関数を「メソッド」と呼びます。
呼び出し可能なユニットのアイデアは、当初、ENIACの研究中にジョン・モークリーとキャスリーン・アントネリによって考案され、1947 年 1 月にハーバード大学で開催された「 EDVAC型マシンの問題の準備」に関するシンポジウムに記録されました。 [ 5 ]モーリス・ウィルクス、デイビッド・ウィーラー、スタンレー・ギルは、一般的にこの概念を正式に発明したとされており、彼らはこれを「閉じたサブルーチン」と呼び、[ 6 ] [ 7 ]開いたサブルーチンやマクロとは対照的でした。[ 8 ]しかし、アラン・チューリングは、1945 年の NPL ACEの設計提案に関する論文でサブルーチンについて議論し、リターン アドレス スタックの概念を発明するまでに至りました。[ 9 ]
サブルーチンの概念は、コンピュータがすでにしばらく存在していた後に考案されました。算術ジャンプ命令と条件付きジャンプ命令は事前に計画されており、比較的変化が少ないですが、プロシージャ呼び出しに使用される特殊命令は長年にわたって大きく変化しました。マンチェスター・ベビーなどの初期のコンピュータや、 RCA 1802などの初期のマイクロプロセッサには、サブルーチン呼び出し命令が1つもありませんでした。サブルーチンを実装することはできましたが、プログラマは呼び出し箇所ごとに呼び出しシーケンス(一連の命令)を使用する必要がありました。
1945年にコンラート・ツーゼのZ4にテープの形で単一のサブルーチンが実装された。 [ 10 ]
1945年、アラン・チューリングはサブルーチンの呼び出しと戻りの手段として「bury」と「unbury」という用語を使用した。[ 11 ] [ 12 ]
1947年1月、ジョン・モークリーは、ハーバード大学と米国海軍兵器局の共同主催による大規模デジタル計算機シンポジウムで概要講演を行った 。その中で彼は、直列および並列演算について論じ、次のように提案している。
機械の構造は、少しも複雑である必要はありません。この手順に必要な論理特性はすべて備わっているため、サブルーチンを機械が認識できるメモリ上の場所に配置し、かつ容易に呼び出し可能な方法で配置するためのコーディング命令を開発することが可能です。
言い換えれば、サブルーチン A を除算、サブルーチン B を複素乗算、サブルーチン C を数値列の標準誤差の評価、といったように、特定の問題に必要なサブルーチンのリストを指定できます。…これらのサブルーチンはすべてマシンに格納され、コーディングで示されている番号で簡単に参照するだけで済みます。[ 5 ]
ケイ・マクナルティはENIACチームでジョン・モークリーと密接に協力し、第二次世界大戦中にプログラミングしていたENIACコンピュータ用のサブルーチンのアイデアを開発した。 [ 13 ]彼女と他のENIACプログラマーは、ミサイルの軌道を計算するためにサブルーチンを使用した。[ 13 ]
ゴールドスタインとフォン・ノイマンは、 1948年8月16日付でサブルーチンの使用について論じた論文を執筆した。[ 14 ]
IBM 1620、Intel 4004、Intel 8008、PICマイクロコントローラなどの初期のコンピュータやマイクロプロセッサには、専用ハードウェアスタックを使用して戻りアドレスを格納する単一命令のサブルーチン呼び出し機能がありました。このようなハードウェアはサブルーチンのネストレベルは数レベルしかサポートしていませんが、再帰サブルーチンはサポートできます。UNIVAC I、PDP-1、IBM 1130など、1960年代半ば以前のマシンでは、通常、呼び出されたサブルーチンの最初のメモリ位置に命令カウンタを保存する呼び出し規約が使用されていました。これにより、任意の深さのサブルーチンネストが可能になりますが、再帰サブルーチンはサポートされていません。IBM System/360には、保存された命令カウンタ値を汎用レジスタに格納するサブルーチン呼び出し命令がありました。追加のコードを使用することで、任意の深さのサブルーチンネストと再帰サブルーチンをサポートできます。 Burroughs B5000 [ 15 ] (1961) は、サブルーチンの戻りデータをスタックに格納した最初のコンピュータの 1 つです。
DEC PDP-6 [ 16 ] (1964 年) は、アキュムレータまたはインデックス レジスタによってアドレス指定されるスタックにリターン アドレスを保存するサブルーチン呼び出し命令を備えた最初のアキュムレータ ベースのマシンの 1 つです。後のPDP-10 (1966 年)、PDP-11 (1970 年)、およびVAX-11 (1976 年) のラインもこれに倣いました。この機能は、任意の深さのサブルーチン ネストと再帰サブルーチンの両方もサポートしています。[ 17 ]
初期のアセンブラでは、サブルーチンのサポートは限られていました。サブルーチンは互いに、あるいはメインプログラムから明示的に分離されておらず、実際、サブルーチンのソースコードが他のサブルーチンのソースコードと混在することもありました。一部のアセンブラでは、呼び出しと戻りのシーケンスを生成するための定義済みマクロが提供されていました。1960年代までには、アセンブラは通常、インラインサブルーチンと個別にアセンブルされたサブルーチンの両方をリンクできる、はるかに高度なサポートを備えるようになりました。
ユーザーが作成したサブルーチンや関数をサポートした最初のプログラミング言語の一つがFORTRAN IIでした。IBM FORTRAN IIコンパイラは1958年にリリースされました。ALGOL 58をはじめとする初期のプログラミング言語も、手続き型プログラミングをサポートしていました。
このような煩雑な方法ではあったものの、サブルーチンは非常に有用であることが証明された。サブルーチンによって、同じコードを多くの異なるプログラムで使用することが可能になったのだ。初期のコンピュータではメモリは非常に貴重なリソースであり、サブルーチンはプログラムのサイズを大幅に削減するのに役立った。
初期のコンピュータの多くは、パンチ紙テープからプログラム命令をメモリにロードしていました。各サブルーチンは、メインプログラム(または「メインライン」[ 18 ])の前または後にロードまたはスプライシングされた別々のテープによって提供され、同じサブルーチンテープを多くの異なるプログラムで使用することができました。パンチカードからプログラム命令をロードするコンピュータでも同様のアプローチが使用されていました。サブルーチンライブラリという名前は、元々は文字通りの意味で、インデックス付きのテープまたはカードのデッキを共同使用のために保管するライブラリを意味していました。
自己書き換えコードの必要性をなくすため、コンピュータ設計者は最終的に間接ジャンプ命令を導入した。この命令のオペランドは、戻りアドレスそのものではなく、戻りアドレスを含む変数またはプロセッサレジスタの位置となる。
これらのコンピュータでは、関数の戻りジャンプを変更する代わりに、呼び出し元のプログラムは戻りアドレスを変数に格納し、関数が完了すると、定義済みの変数で指定された場所に実行を誘導する間接ジャンプを実行します。
もう一つの進歩は、サブルーチンへのジャンプ命令である。これは、戻りアドレスの保存と呼び出し元のジャンプを組み合わせることで、オーバーヘッドを大幅に最小限に抑えた。1950年代には、少なくとも5種類のサブルーチンへのジャンプ命令が存在した。
IBM System/360アーキテクチャは、戻りアドレスを指定されたレジスタに保存する例です。プロシージャ呼び出し用に設計された分岐命令BALまたはBALRは、命令で指定されたプロセッサレジスタ(慣例としてレジスタ14)に戻りアドレスを保存します。サブルーチンは、戻るために、そのレジスタを介して間接分岐命令(BR)を実行するだけで済みます。サブルーチンがそのレジスタを他の目的(別のサブルーチンの呼び出しなど)で使用する必要がある場合は、レジスタの内容をプライベートメモリ位置またはレジスタスタックに保存します。
HP 2100アーキテクチャは、サブルーチンの最初のワードにリターンアドレスを保存する例です。JSB命令は、分岐先のメモリ位置にリターンアドレスを保存します。プロシージャの実行は、次のメモリ位置から開始されます。HP 2100アセンブリ言語では、例えば次のように記述します。
... JSB MYSUB(サブルーチンMYSUBを呼び出します。) BB…(MYSUBが完了したらここに戻ってきます。) メインプログラムからMYSUBというサブルーチンを呼び出す。サブルーチンは次のようにコーディングされる。
MYSUB NOP(MYSUBの返信先アドレスを格納する場所。) AA ... (MYSUB本体の開始。) ... JMP MYSUB,I(呼び出し元のプログラムに戻ります。) JSB命令は、NEXT命令(BB)のアドレスをオペランドとして指定された場所(MYSUB)に配置し、その後、次のNEXTアドレス(AA = MYSUB + 1)に分岐しました。サブルーチンは、間接ジャンプ命令JMP MYSUB, Iを実行することでメインプログラムに戻ることができました。この命令は、MYSUBに格納されているアドレスに分岐します。Fortranやその他の言語のコンパイラは、これらの命令が利用可能であれば容易に利用できました。この方式は複数レベルの呼び出しをサポートしていましたが、サブルーチンの戻りアドレス、パラメータ、戻り値は固定メモリ位置に割り当てられていたため、再帰呼び出しはできませんでした。
ちなみに、 1980年代初頭にLotus 1-2-3でも同様の手法が用いられ、表計算ソフト内の再計算依存関係を検出していました。具体的には、各セルにリターンアドレスを格納するための領域が確保されていました。自然な再計算順序では循環参照が許容されないため、この手法ではメモリにスタック領域を確保することなくツリー構造を走査することが可能でした。これは、 IBM PCのような小型コンピュータではメモリ容量が非常に限られていたため、非常に重要な利点でした。
関数呼び出しの最新の実装のほとんどは、スタックデータ構造の特殊なケースであるコールスタックを使用して、関数呼び出しと戻り値を実装します。各プロシージャ呼び出しは、スタックの最上位にスタックフレームと呼ばれる新しいエントリを作成します。プロシージャが戻ると、そのスタックフレームはスタックから削除され、その領域は他のプロシージャ呼び出しに使用できます。各スタックフレームには、対応する呼び出しのプライベートデータが含まれており、通常はプロシージャのパラメータと内部変数、および戻りアドレスが含まれます。
呼び出しシーケンスは、通常の命令のシーケンスによって実装することもできます(これは、 RISC( Reduced Instruction Set Computing)やVLIW( Very Long Instruction Word)アーキテクチャで現在でも使用されているアプローチです)が、1960年代後半以降に設計された多くの従来型マシンには、その目的のための特別な命令が含まれています。
コールスタックは通常、連続したメモリ領域として実装されます。スタックの最下部がこの領域内の最低アドレスか最高アドレスかは任意の設計上の選択であり、スタックはメモリ内で前方または後方に拡張できますが、多くのアーキテクチャは後者を選択しています。[ 24 ]
一部の設計、特にForthの実装では、2つの独立したスタックが使用されていました。1つは主に制御情報(リターンアドレスやループカウンタなど)用、もう1つはデータ用です。前者はコールスタックとして機能し、プログラマは他の言語構造を介して間接的にしかアクセスできませんでしたが、後者はより直接的にアクセスできました。
スタックベースのプロシージャ呼び出しが最初に導入されたとき、重要な動機の一つは貴重なメモリを節約することでした。[ 25 ]この方式では、コンパイラは各プロシージャのプライベートデータ(パラメータ、戻りアドレス、ローカル変数)のためにメモリに個別の領域を確保する必要がありません。スタックには、常に現在アクティブな呼び出し(つまり、呼び出されたがまだ戻っていない呼び出し)のプライベートデータのみが含まれます。プログラムが通常ライブラリから組み立てられる方法のため、数千の関数を含むプログラムで、そのうちのごく一部だけが特定の時点でアクティブになっていることは珍しくありませんでした(そして今でもそうです)。このようなプログラムでは、コールスタックメカニズムによってかなりの量のメモリを節約できます。実際、コールスタックメカニズムは、自動メモリ管理の最も初期かつ最も単純な方法と見なすことができます。
しかし、コールスタック方式のもう1つの利点は、再帰的な関数呼び出しが可能であることです。なぜなら、同じプロシージャへのネストされた呼び出しごとに、そのプライベートデータの個別のインスタンスが取得されるからです。
マルチスレッド環境では、一般的にスタックは複数存在します。[ 26 ]コルーチンや遅延評価を完全にサポートする環境では、スタック以外のデータ構造を使用してアクティベーションレコードを格納する場合があります。
コールスタック機構の欠点の1つは、プロシージャ呼び出しとその戻り値のコストが増加することです。この追加コストには、スタックポインタのインクリメントとデクリメント(および、一部のアーキテクチャではスタックオーバーフローのチェック)、および絶対アドレスではなくフレーム相対アドレスによるローカル変数とパラメータへのアクセスが含まれます。このコストは、実行時間の増加、プロセッサの複雑性の増加、またはその両方として現れる可能性があります。
このオーバーヘッドは、自身ではプロシージャ呼び出しを行わずに戻るリーフプロシージャまたはリーフ関数で最も顕著で問題となります。 [ 27 ] [ 28 ]このオーバーヘッドを削減するために、多くの最新のコンパイラは、コールスタックの使用を実際に必要になるまで遅らせようとします。たとえば、プロシージャPの呼び出しは、呼び出されたプロシージャの戻りアドレスとパラメータを特定のプロセッサレジスタに格納し、単純なジャンプによってプロシージャ本体に制御を移すことができます。プロシージャP が他の呼び出しを行わずに戻る場合、コールスタックは全く使用されません。Pが別のプロシージャQを呼び出す必要がある場合、 Q が戻った後に必要となるレジスタの内容 (戻りアドレスなど) を保存するためにコールスタックを使用します。
一般的に、呼び出し可能なユニットとは、最初の命令から順に実行される命令のリストであり、内部ロジックによって指示される場合を除き、順番に実行されます。プログラムの実行中に何度でも呼び出すことができます。呼び出し命令が制御を戻すと、実行は呼び出し命令の次の命令から再開されます。
呼び出し可能なユニットの実装の特徴は、時間の経過とともに進化し、状況によっても異なります。このセクションでは、一般的なさまざまな実装の特徴について説明します。
ほとんどの現代のプログラミング言語は、関数を定義したり呼び出したりするための機能を提供しており、それらの機能にアクセスするための構文も含まれています。例えば、以下のようなものがあります。
Pascal、Fortran、Ada 、 BASICの多くの方言など、一部の言語では、値を返す呼び出し可能なユニット(関数またはサブルーチン)と、値を返す呼び出し可能なユニット(サブルーチンまたはプロシージャ)に異なる名前を使用します。C 、C++、C#、Lispなどの他の言語では、呼び出し可能なユニットに関数という1つの名前のみを使用します。C系の言語では、戻り値がないことを示すためにキーワードを使用します。void
値を返すように宣言されている場合、戻り値を使用するために、呼び出しを式の中に埋め込むことができます。たとえば、平方根呼び出し可能なユニットは、次のように呼び出すことができますy = sqrt(x)。
値を返さない呼び出し可能なユニットは、`callable`のように単独のステートメントprint("hello")として呼び出されます。この構文は、値を返す呼び出し可能なユニットにも使用できますが、戻り値は無視されます。
古い言語の中には、戻り値を使用しない呼び出しにキーワードを必要とするものがありますCALL print("hello")。
ほとんどの実装、特に現代のプログラミング言語では、呼び出し可能なオブジェクトが仮引数として宣言するパラメータをサポートしています。呼び出し元は、実際のパラメータ(引数とも呼ばれる)を対応するオブジェクトに渡します。プログラミング言語によって、引数の受け渡し方法は異なります。
BASICなどの一部の言語では、呼び出し可能オブジェクトは、値を返す場合と返さない場合で構文(キーワード)が異なります。他の言語では、構文はどちらの場合も同じです。C void、C++、C#などの一部の言語では、戻り値がないことを宣言するために追加のキーワードが使用されます。Pythonなどの一部の言語では、違いは本体に値を持つreturn文が含まれているかどうかであり、特定の呼び出し可能オブジェクトは制御フローに基づいて値を返す場合と返さない場合があります。
多くの状況において、呼び出し可能なオブジェクトは、渡されたデータやグローバルデータの変更、周辺機器からの読み書き、ファイルへのアクセス、プログラムやマシンの停止、プログラム実行の一時停止など、副作用を伴う動作を行う可能性があります。
設計原則の推進で知られるロバート・C・マーティンは、副作用は望ましくないと考えている。マーティンは、副作用によって時間的結合や順序依存性が生じる可能性があると主張している。[ 29 ]
Haskellのような厳密な関数型プログラミング言語では、関数は副作用を持つことができません。つまり、プログラムの状態を変更することはできません。関数は常に同じ入力に対して同じ結果を返します。このような言語は通常、値を返す関数のみをサポートします。なぜなら、戻り値も副作用もない関数には価値がないからです。
ほとんどのコンテキストでは、ローカル変数(呼び出し可能なオブジェクトが所有するメモリ領域で、中間値を保持するために使用される)がサポートされています。これらの変数は通常、戻りアドレスなどの他の情報とともに、呼び出しスタック上の呼び出しのアクティベーションレコードに格納されます。
言語がサポートしていれば、呼び出し可能なオブジェクトは自身を呼び出すことができ、その結果、同じ呼び出し可能なオブジェクトの別のネストされた実行が実行されている間、自身の実行が中断されます。再帰は、複雑なアルゴリズムを簡略化し、複雑な問題を分解するのに役立つ手段です。再帰言語は、呼び出しごとにローカル変数の新しいコピーを提供します。プログラマが再帰呼び出し可能なオブジェクトでローカル変数の代わりに同じ変数を使用したい場合は、通常、静的またはグローバルなどの共有コンテキストでそれらを宣言します。
ALGOL、PL/I、Cといった言語から現代の言語に至るまで、ほぼ例外なくコールスタックが用いられています。これは通常、命令セットによってサポートされ、各呼び出しのアクティベーションレコードを提供します。こうすることで、ネストされた呼び出しは、中断された呼び出しの変数に影響を与えることなく、自身のローカル変数を変更できます。
再帰を用いることで、数学的帰納法や再帰的な分割統治アルゴリズムによって定義された機能を直接実装できます。以下は、フィボナッチ数列を求めるC言語の再帰関数の例です。
int fibonacci ( unsigned int n ) { if ( n <= 1 ) { return n ; } return fibonacci ( n - 1 ) + fibonacci ( n - 2 ); }Fortranのような初期の言語は、呼び出し可能な関数ごとに1組の変数と戻りアドレスしか割り当てられていなかったため、当初は再帰をサポートしていませんでした。[ 30 ]初期のコンピュータ命令セットでは、戻りアドレスと変数をスタックに格納することが困難でした。インデックスレジスタまたは汎用レジスタを備えたマシン(例:CDC 6000シリーズ、PDP-6、GE 635、System/360、UNIVAC 1100シリーズ)は、これらのレジスタの1つをスタックポインタとして使用することができました。
Ada、Pascal、PL/I、Pythonなどの一部の言語では、関数本体などの内部で関数を宣言および定義することができ、内部の関数の名前は外部の関数本体内でのみ表示されます。
Pascalでの簡単な例:
function E ( x : real ) : real ; function F ( y : real ) : real ; begin F := x + y end ; begin E := F ( 3 ) + F ( 4 ) end ;この関数Fは の中にネストされていますE。Eのパラメータはxの中にも表示されますがF(Fは の一部であるためE) x、 と はそれぞれ と のy外部では表示されませEんF。
呼び出し可能なプログラムが、同じ呼び出し可能なプログラムの別の実行が既に進行中であっても正しく実行できる場合、その呼び出し可能なプログラムは再入可能であると言われます。再入可能な呼び出し可能なプログラムは、複数のスレッドが互いに干渉する心配なく同じ呼び出し可能なプログラムを呼び出すことができるため、マルチスレッド環境でも有用です。IBM CICSトランザクション処理システムでは、準再入可能性は、多くのスレッドで共有されるアプリケーションプログラムに対する、やや制約は緩いものの類似した要件でした。
一部の言語ではオーバーロードがサポートされており、同じスコープ内で同じ名前の複数の呼び出し可能オブジェクトを、異なるタイプの入力に対して動作させることができます。実数、複素数、行列の入力に適用される平方根関数を考えてみましょう。入力タイプのごとにアルゴリズムが異なり、戻り値の型も異なる場合があります。同じ名前の呼び出し可能オブジェクトを 3 つ別々に記述することで、例えばsqrt のように記述すると、 sqrt_real、sqrt_complex、qrt_matrixのような長くて複雑な名前ではなく、それぞれが比較的理解しやすく覚えやすい名前になるため、結果として得られるコードは記述しやすく、保守しやすくなります。
オーバーロードは、厳密な型付けをサポートする多くの言語でサポートされています。多くの場合、コンパイラは入力引数の型に基づいて呼び出すオーバーロードを選択します。入力引数がオーバーロードを選択しない場合は、コンパイルエラーが発生します。古い言語や弱い型付けの言語は、一般的にオーバーロードをサポートしていません。
以下はC++におけるオーバーロードの例として、area異なる型を受け入れる2つの関数を示します。
// 高さと幅で定義される長方形の面積を返しますdouble area ( double height , double width ) { return h * w ; }// 半径で定義された円の面積を返しますdouble area ( double radius ) { return radius * radius * std :: numbers :: pi ; }int main () { double rectangleArea = area ( 3 , 4 ); double circleArea = area ( 5 ); }PL/Iには、GENERIC異なる型の引数で呼び出されるエントリ参照のセットに対して汎用名を定義する機能があります。例:
DECLARE gen_name GENERIC( 名前 WHEN(固定バイナリ)、 炎 WHEN(FLOAT)、 パス名(それ以外の場合)
各エントリに対して、複数の引数定義を指定できます。「gen_name」を呼び出すと、引数がFIXED BINARYの場合は「name」が、FLOATの場合は「flame」が呼び出されます。引数がいずれの選択肢にも一致しない場合は、「pathname」が呼び出されます。
クロージャとは、呼び出し可能なオブジェクトに、それが作成された環境から取得した変数の値の一部を加えたものです。クロージャは、ジョン・マッカーシーによって導入されたLispプログラミング言語の注目すべき特徴でした。実装によっては、クロージャは副作用のメカニズムとして機能することもあります。
正常な動作に加えて、呼び出し可能なオブジェクトは、実行中に発生した例外的な状況を呼び出し元に通知する必要がある場合がある。
現代のプログラミング言語のほとんどは例外処理をサポートしており、例外ハンドラが見つかるまでコールスタックをポップして例外的な制御フローを処理することが可能です。
例外処理をサポートしていない言語では、戻り値を使って呼び出しの成功または失敗を示すことができます。別の方法としては、グローバル変数などの既知の場所を成功の指標として使用する方法があります。呼び出し可能なオブジェクトが値を書き込み、呼び出し元は呼び出し後にその値を読み取ります。
IBM System/360では、サブルーチンから戻りコードが返される場合、戻り値は4の倍数になるように設計されることが多かった。これは、呼び出し命令の直後に配置されることが多い分岐テーブルへの直接的なインデックスとして使用することで、余分な条件テストを回避し、効率をさらに向上させるためである。System /360アセンブリ言語では、例えば次のように記述する。
BAL 14、SUBRTN01 サブルーチンへ移動し、戻りアドレスを R14 に格納する B TABLE(15) レジスタ 15 の戻り値を使用してブランチ テーブルのインデックスを作成します。 * 適切な分岐命令に分岐します。 テーブルB OK リターンコード = 00 良好 } B 不良リターンコード = 04 無効な入力 } 分岐テーブル B エラー リターンコード = 08 予期しない状態 } 呼び出しには実行時オーバーヘッドがあり、これには以下が含まれますが、これらに限定されません。
呼び出しの実行時コストを最小限に抑えるために、さまざまな手法が用いられています。
呼び出しオーバーヘッドを最小限に抑えるための最適化の中には、一見単純に見えるものもありますが、呼び出し可能な関数に副作用がある場合は使用できません。たとえば、式 では(f(x)-1)/(f(x)+1)、関数をf一度だけ呼び出してその値を二度使用することはできません。なぜなら、二度の呼び出しで異なる結果が返される可能性があるからです。さらに、除算演算子のオペランドの評価順序を定義する言語はごく少数ですが、x最初の呼び出しで の値が変更されている可能性があるため、二度目の呼び出しの前に の値を再度取得する必要があります。呼び出し可能な関数に副作用があるかどうかを判断するのは困難です。実際、ライスの定理により決定不能です。したがって、この最適化は純粋な関数型プログラミング言語では安全ですが、関数型に限定されない言語のコンパイラは通常、最悪のケース、つまりすべての呼び出し可能な関数に副作用がある可能性があると想定します。
インライン化は、特定の呼び出し可能オブジェクトへの呼び出しをなくします。コンパイラは、各呼び出しを呼び出し可能オブジェクトのコンパイル済みコードに置き換えます。これにより、呼び出しのオーバーヘッドが回避されるだけでなく、呼び出し時のコンテキストと引数を考慮することで、コンパイラが呼び出し元のコードをより効果的に最適化できるようになります。ただし、インライン化は通常、コンパイル済みコードのサイズを増加させます。ただし、呼び出しが1回のみの場合や、本体が1行のように非常に短い場合は例外です。
呼び出し可能なオブジェクトは、プログラム内で定義することも、複数のプログラムで使用できるライブラリ内で個別に定義することもできます。
コンパイラは、明確に定義された呼び出し規約に従って、呼び出し文と戻り文を機械語命令に変換します。同じコンパイラまたは互換性のあるコンパイラでコンパイルされたコードの場合、関数は、それを呼び出すプログラムとは別にコンパイルできます。呼び出し文と戻り文に対応する命令シーケンスは、プロシージャのプロローグとエピローグと呼ばれます。
組み込み関数、または組み込み関数、または内在関数とは、コンパイラがコンパイル時にコードを生成するか、他の関数とは異なる方法で提供する関数のことです。[ 31 ] 組み込み関数はプログラミング言語に組み込まれているため、他の関数のように定義する必要はありません。 [ 32 ]
C++、Rust、Zigなどの一部のプログラミング言語では、関数をコンパイル時に評価できます。これにより、追加のスタックフレームを使用しない、メモリ使用量を削減する、戻り値をバイナリコード自体に埋め込むなどの方法で実行時のオーバーヘッドが削減されます。[ 33 ]
プログラムを関数に分割することの利点は以下のとおりです。
インラインコードを使用する場合と比較して、関数を呼び出すと、呼び出しメカニズムにおいてある程度の計算オーバーヘッドが発生します。
関数には通常、関数の開始時と終了時の両方で標準的な管理コードが必要です(関数のプロローグとエピローグ。通常は、汎用レジスタと戻りアドレスを最低限保存します)。
呼び出し可能なオブジェクトに関して、多くのプログラミング規約が開発されてきた。
命名に関して言えば、多くの開発者は、特定のタスクを実行する呼び出し可能なオブジェクトには動詞で始まるフレーズを、問い合わせを行うオブジェクトには形容詞で始まるフレーズを、変数の置換に使用されるオブジェクトには名詞で始まるフレーズを命名します。
プログラマーの中には、呼び出し可能な関数は正確に1つのタスクのみを実行すべきであり、複数のタスクを実行する場合は複数の呼び出し可能な関数に分割すべきだと主張する者もいる。彼らは、呼び出し可能な関数はソフトウェア保守における重要な要素であり、プログラム内での役割は明確に区別されなければならないと主張する。
モジュール型プログラミングの提唱者は、各呼び出し可能オブジェクトはコードベースの他の部分への依存を最小限に抑えるべきだと主張します。例えば、グローバル変数の使用は、グローバル変数を使用するすべての呼び出し可能オブジェクト間の結合度を高めるため、一般的に賢明ではないとされています。そのような結合が不要な場合は、呼び出し可能オブジェクトをリファクタリングして、代わりに引数として渡されるパラメータを受け入れるようにすることを推奨しています。
初期のBASICでは、各行に一意の番号(行番号)が必要で、これにより実行順序が決まります。呼び出し可能なコードの分離はなく、引数を渡したり値を返すメカニズムもなく、すべての変数はグローバルです。コマンドは、sub procedure、subprocedure、またはsubroutineの略ですGOSUB。指定された行番号に制御がジャンプし、戻ると次の行に進みます。
10 REMベーシックプログラム20 GOSUB 100 30 GOTO 20 100 INPUT “数値を入力してください” ; N 110 PRINT “の平方根” ; N ; 120 PRINT “は” ; SQRT ( N ) 130 RETURNこのコードは、ユーザーに繰り返し数値を入力するように求め、その値の平方根を報告します。100行目から130行目が呼び出し可能な部分です。
Microsoft Small Basicは、テキストベースの言語でプログラミングを初めて学ぶ学生を対象としており、呼び出し可能な単位はサブルーチンと呼ばれます。Subキーワードはサブルーチンの開始を示し、その後に名前識別子が続きます。後続の行は本体であり、EndSubキーワードで終わります。[ 34 ]
サブSayHello TextWindow 。WriteLine ( "こんにちは!" ) EndSubこれは次のように呼ばれるSayHello()。 [ 35 ]
最新の製品ラインやVB6を含む、 Visual Basic (VB)の後のバージョンでは、呼び出し可能な単位の概念を表すのにプロシージャという用語が使われています。キーワードは、値を返さないことと値を返すことの両方に使用されます。クラスのコンテキストで使用される場合、プロシージャはメソッドです。 [ 36 ]SubFunction
各パラメーターには指定可能なデータ型がありますが、指定しない場合は、 .NETObjectに基づく後のバージョンではデフォルト値が、VB6ではバリアントがデフォルト値になります。[ 37 ]
VB は、キーワード と を介して、値渡しと参照渡しによるパラメータの受け渡し規則をサポートしています。が指定されていない限り、引数は で渡されます。したがって、が明示的に指定されることはほとんどありません。ByValByRefByRefByValByVal
数値のような単純な型の場合、これらの規則は比較的明確です。引数として渡すと、プロシージャはByRef渡された変数を変更できますが、引数として渡しても変更できません。オブジェクトの場合、オブジェクトは常に参照として扱われるため、セマンティクスはプログラマを混乱させる可能性があります。オブジェクトを渡すと、オブジェクトの状態ではなく参照がコピーされます。呼び出されたプロシージャは、メソッドを介してオブジェクトの状態を変更できますが、実引数のオブジェクト参照を変更することはできません。ByValByVal
Sub DoSomething () ' ここにコードEnd Subこれは値を返さず、次のように単独で呼び出す必要があります。DoSomething
Function GiveMeFive () as Integer GiveMeFive = 5 End Functionこれは値5を返し、呼び出しは次のような式の一部になります。y = x + GiveMeFive()
Sub AddTwo ( ByRef intValue as Integer ) intValue = intValue + 2 End Subこれには副作用があります。参照渡しされた変数が変更され、v次のような変数に対して呼び出される可能性がありますAddTwo(v)。呼び出し前のvが5の場合、呼び出し後は7になります。
C言語およびC++言語では、呼び出し可能な単位を関数と呼びます。関数定義は、関数が返す値の型名、またはvoid値を返さないことを示す名前で始まります。その後に、関数名、括弧で囲まれた仮引数、中括弧で囲まれた本体が続きます。
C++では、クラス内で宣言された関数(非静的)はメンバ関数またはメソッドと呼ばれます。クラスの外にある関数は、メンバ関数と区別するためにフリー関数と呼ばれることがあります。[ 38 ]
void doSomething () { // 何らかのコード}この関数は値を返さず、常に単独で呼び出されます。doSomething()
int returnFive () { return 5 ; }この関数は整数値5を返します。呼び出しは単独で行うことも、次のような式の中で行うこともできます。y = x + returnFive()
void addTwo ( int * pi ) { * pi += 2 ; }この関数には副作用があります。アドレスで渡された値を、入力値に2を加えた値に変更します。変数に対しては、次vのように呼び出すことができますaddTwo(&v)。ここで、アンパサンド(&)はコンパイラに変数のアドレスを渡すように指示します。呼び出し前のvの値が5の場合、呼び出し後は7になります。
void addTwo ( int & i ) { i += 2 ; }この関数は C++ を必要とします。Cではコンパイルできません。前の例と同じ動作をしますが、アドレスではなく実際のパラメータを参照渡しします。 のような呼び出しにはaddTwo(v)アンパサンドは含まれません。コンパイラが呼び出しに構文がなくても参照渡しを処理するためです。
PL/Iでは、呼び出されるプロシージャに、文字列の長さや配列の境界など、引数に関する情報を提供する記述子を渡すことができます。これにより、プロシージャの汎用性が向上し、プログラマがそのような情報を渡す必要がなくなります。デフォルトでは、PL/Iは引数を参照渡しします。2次元配列の各要素の符号を変更する(簡単な)関数は次のようになります。
change_sign : procedure ( array ) ; declare array (*,*) float ; array = - array ; end change_sign ;これは、以下のように様々な配列で呼び出すことができます。
/* 最初の配列の範囲は -5 から +10 と 3 から 9 です */ declare array1 (- 5 : 10 , 3 : 9 ) float ; /* 2 番目の配列の範囲は 1 から 16 と 1 から 16 です */ declare array2 ( 16 , 16 ) float ; call change_sign ( array1 ) ; call change_sign ( array2 ) ;Pythonでは、キーワードはdef関数定義の開始を示します。関数本体のステートメントは、後続の行にインデントされたとおりに続き、最初の行と同じインデントされた行またはファイルの末尾で終了します。[ 39 ]
def format_greeting ( name : str ) -> str : return f "ようこそ、{ name } !"def greet_martin () -> None : print ( format_greeting ( "Martin" ))最初の関数は、呼び出し元から渡された名前を含む挨拶文を返します。2番目の関数は最初の関数を呼び出し、greet_martin()コンソールに「ようこそ、マーティン」と表示するのと同じように呼び出されます。
論理プログラムの手続き的解釈において、論理的含意は目標削減手続きとして振る舞う。次の形式の規則(または節)は次のようになる。
A :- B論理的に解釈すると次のようになる。
A if Bは、 と統一するA目標を、のインスタンスであるサブ目標に縮小 する手順として機能しますB。
例えば、Prologプログラムを考えてみましょう。
mother_child ( elizabeth , charles ). father_child ( charles , william ). father_child ( charles , harry ). parent_child ( X , Y ) :- mother_child ( X , Y ). parent_child ( X , Y ) :- father_child ( X , Y ).母性機能は、関係データベースと同様に、関係によって表現されることに注意してください。ただし、Prolog における関係は呼び出し可能な単位として機能します。X=mother(Y)
例えば、このプロシージャ呼び出しでは、出力が生成されます。しかし、同じプロシージャを他の入出力パターンで呼び出すこともできます。例えば、次のようになります。?-parent_child(X,charles)X=elizabeth
?- parent_child ( elizabeth , Y ). Y = charles .?- parent_child ( X , Y ). X = elizabeth , Y = charles .X =チャールズ、Y =ハリー。X =チャールズ、Y =ウィリアム。?- parent_child ( william , harry ). no 。?- parent_child ( elizabeth , charles ). yes 。呼び出し可能な単位:(ソフトウェアプログラムまたは論理設計について)モジュール内に現れる関数、メソッド、操作、サブルーチン、プロシージャ、または類似の構造単位。
We could supply our assembling clerk with copies of the source code for all of our useful subroutines and then when presenting him with a mainline program for assembly, tell him which subroutines will be called in the mainline [...]