コンピュータプログラミングにおいて、ネストされた関数(またはネストされたプロシージャ、サブルーチン)とは、別の(囲み)ブロック内で定義され、囲みブロック内で字句スコープを持つ名前付き関数のことです。つまり、囲みブロックの本体内でのみ名前で呼び出すことができ、外側のブロック(外側の関数を含む)で宣言された識別子を使用できます。囲みブロックは通常、別の関数ですが、常にそうとは限りません。
ネストされた関数に対するプログラミング言語のサポート状況は様々です。構造化プログラミング言語では、ALGOL、Simula 67、Pascalといった一部の旧式言語や、広く使われているJavaScriptでサポートされています。動的言語や関数型言語では一般的にサポートされています。しかし、標準CやC++など、広く使われている言語の中にはサポートされていないものもあります。
他のプログラミング技術も同様の利点を提供します。例えば、ラムダ関数では、関数内(および他の場所)で関数を定義することができ、同様のデータ隠蔽とカプセル化が可能です。特筆すべきは、ラムダ関数には名前がなく(匿名)、そのため名前で呼び出すことができず、可視性もありません。
ネストされた関数のスコープは、その関数を含むブロック(関数ブロックまたは関数本体内のブロック)です。包含するブロックの外からは参照できません(名前で呼び出すことはできません)。
ネストされた関数は、同じ名前の内部宣言によってマスクされている場合を除き、任意の囲みブロックで宣言された識別子(関数、変数、型、クラスの名前など)を使用できます。
ネストされた関数は、再帰的にネストされた関数の中に宣言することができ、深くネストされた構造を形成できます。深くネストされた関数は、それを囲むすべてのブロック(囲む関数を含む)で宣言された識別子にアクセスできます。
ネストされた関数は、特定の状況下でクロージャの作成につながる可能性があります。ネストされた関数が囲んでいる関数から脱出できる場合、たとえば関数が第一級オブジェクトであり、ネストされた関数が別の関数に渡されるか、囲んでいる関数から返される場合、クロージャが作成され、この関数への呼び出しは元の関数の環境にアクセスできます。直近の囲んでいる関数のフレームは、最後に参照しているクロージャが終了するまで生存し続ける必要があり、クロージャ内で参照される非ローカル自動変数は、囲んでいるブロックの生存期間を超えてクロージャが存続することを許可する言語ではスタックに割り当てることができません。これはfunarg問題として知られており、ネストされた関数が一部の単純な言語で実装されなかった主な理由です。これは、特に関数がさまざまなレベルでネストされ、環境の異なる部分を共有する場合に、コード生成と分析を著しく複雑にするためです。
ネストされた関数技術を用いることで、プログラマーは情報隠蔽、カプセル化、分解といった有益な特性を備えたソースコードを作成できます。プログラマーはタスクをサブタスクに分割することができ、各サブタスクはタスクのコンテキスト内でのみ意味を持ち、サブタスク関数はそれらを使用するように設計されていない呼び出し元からは隠蔽されます。
ブロックスコープを使用すると、関数はパラメータを渡したりグローバル変数を使用したりすることなく、囲んでいるブロック(囲んでいる関数を含む)の状態を共有できます。[ 1 ]
ネストされた関数は、一般的な非構造化制御フローに return 文を使用することで、非構造化制御フローに使用できます。これは、言語の他の組み込み機能よりもきめ細かい制御に使用できます。たとえば、 が利用できない場合に for ループを早期に終了したり、多段階の や例外が利用できない場合にネストされたfor ループbreakを早期に終了したりできます。break
一部の言語では、外側の関数から一連のパラメータにアクセスするネストされた関数(クロージャ)を作成し、その関数を外側の関数の戻り値にすることができます。そのため、ほとんどまたは全く追加のパラメータを与えずに特定のタスクを実行するように設定された関数を返すことが可能になり、パフォーマンスを大幅に向上させることができます。[ 2 ]
A simple example in Pascal:
functionE(x:real):real;functionF(y:real):real;beginF:=x+yend;beginE:=F(3)+F(4)end;The function F is nested within E. Note that E's parameter x is also visible in F (as F is a part of E) while both x and y are invisible outside E and F respectively.
Similarly, in Standard ML:
fune(x:real)=letfunfy=x+yinf3+f4end;In Haskell:
e::Float->Floatex=f3+f4wherefy=x+yIn PL/I:
e: procedure(x) returns(float); declare x float; f: procedure(y) returns(float); declare y float; return x + y end; return f(3.0) + f(4.0); end;
In Python:
defe(x:float)->float:deff(y:float)->float:returnx+yreturnf(3.0)+f(4.0)In GNU C[3]– which extends standard C with nested functions:
floate(floatx){floatf(floaty){returnx+y;}returnf(3.0f)+f(4.0f);}void sort ( int * items , int size ) { void quickSort ( int first , int last ) { void swap ( int p , int q ) { int tmp = items [ p ]; items [ p ] = items [ q ]; items [ q ] = tmp ; } int partition () { int pivot = items [ first ]; int index = first ; swap ( index , last ); for ( int i = first ; i < last ; i ++ ) { if ( items [ i ] < pivot ) { swap ( index ++ , i ); } } swap ( index , last ); return index ; }if ( first < last ) { int pivotIndex = partition (); quickSort ( first , pivotIndex - 1 ); quickSort ( pivotIndex + 1 , last ); } } quickSort ( 0 , size - 1 ); }以下は、関数の中に関数を隠すことも可能にする代替技術であるC++11ラムダ式構文を使用した、 Hoare分割ベースクイックソートの実装例です。
// Iter はランダムアクセス イテレータを表しますtemplate < typename Iter > void sort ( Iter begin , Iter end ) { auto partition = [ & ]() -> Iter { // Hoare パーティション スキームIter & pivot = * begin ; Iter forwardCursor = begin ; Iter backwardCursor = end - 1 ; Iter partitionPositionFound = false ;auto locatePartitionPosition = [ & ]() -> void { while ( * forwardCursor < pivot ) { ++ forwardCursor ; } while ( pivot < * backwardCursor ) { -- backwardCursor ; } if ( forwardCursor >= backwardCursor ) { partitionPositionFound = true ; } else swap ( * forwardCursor , * backwardCursor ); } };// 簡単なヘルパー関数auto moveOnAndTryAgain = [ & ]() -> void { ++ forwardCursor ; -- backwardCursor ; };// 実際のパーティション処理の簡単な概要while ( true ) { locatePartitionPosition (); if ( partitionPositionFound ) return backwardCursor + 1 ; else { moveOnAndTryAgain (); } } };// クイックソートアルゴリズムの簡単な概要if ( begin < end - 1 ) { Iter partitionPosition = partition (); sort ( begin , partitionPosition ); sort ( partitionPosition , end ); } }ネストされた関数をサポートする代表的なプログラミング言語には、以下のようなものがあります。
Schemeなどのほとんどの関数型プログラミング言語では、ループを含むアルゴリズムを実装する際に、関数のネストが一般的な方法となります。アルゴリズムのメインループとして機能する単純な(末尾)再帰的な内部関数を作成し、外部関数は一度だけ実行すればよい起動処理を行います。より複雑なケースでは、相互に再帰的な関数を複数内部関数として作成することもあります。
ネストされた関数を用いる場合と同様のプログラミング結果を得るために、さまざまな代替手法を用いることができる。
一般的な代替手段として、言語のモジュール化技術を活用する方法があります。一部の関数はモジュールの外部から使用できるように公開され、一部の関数はモジュール内でのみ使用可能です。
C言語では、関数や変数をstaticとして宣言することで、ファイル外のコードからそれらを隠蔽し、これを実現できます。[ 10 ]これにより、データの隠蔽、カプセル化、分解が可能になりますが、ネストされた関数とは異なる粒度になります。このモジュール性では、1レベルを超えるネストはサポートされません。
オブジェクト指向言語では、クラスは通常、関数や状態をクラスの利用者からは隠蔽しつつ、クラス内部からはアクセス可能なスコープを提供する。一部の言語では、クラスのネストが可能である。
データ隠蔽を実装するために、関数は共有データをパラメータとして渡すことができますが、これは関数呼び出しの複雑さを増大させます。[ 1 ]
C言語では、これは一般的に共有データを含む構造体へのポインタを渡すことによって実装されます。[ 10 ]
PHPなどの言語では、ラムダ式が代替手段となります。ラムダ式では、通常の関数構文で宣言するのではなく、コードステートメント内で関数を定義します。ラムダ式には名前はありませんが、関数参照を介して呼び出すことができます。このような関数は、関数内だけでなく、他のスコープでも定義できます。匿名関数内でローカル変数を使用するには、クロージャを使用します。
以下の言語は、ネストされた関数に似た機能を提供します。
ネストされた関数の実装は、見た目以上に複雑になる場合があります。非ローカル変数を参照するネストされた関数への参照はクロージャを作成するためです。このため、ネストされた関数は、コンパイラの実装が難しくなるため、C、C++、Javaなどの一部の言語ではサポートされていません。[ 10 ] [ 13 ]ただし、コンパイラ固有の拡張機能として、ネストされた関数をサポートするコンパイラもあります。よく知られている例としては、Pascal、Ada、Modulaなどの言語のコンパイラとコードを共有するCのGNU C実装があります。
字句スコープ言語でネストされた手続きを実装する方法はいくつかありますが、古典的な方法は次のとおりです。
このオリジナルの方法は見た目よりも高速ですが、それでも現代の実用的なコンパイラでは(ディスプレイ表示や同様の技術を用いて)最適化されることがよくあります。
コンパイラによっては、ネストされた関数を実装する別の方法として、コンパイルの中間段階でラムダリフティングと呼ばれるプロセスを用いて、ネストされた関数を非ネスト関数に変換(「リフト」)する方法があります(この場合、アクセスリンクの代わりに余分な隠しパラメータが使用されます)。
字句スコープの非ローカル変数を持つローカル関数を結果として渡すためには、言語ランタイムコードは、関数がカプセル化関数内で参照する環境(データ)も暗黙的に渡す必要があり、これにより、囲んでいる関数の現在の実行がなくなった場合でも、その環境にアクセスできるようになります。 [ 14 ]これは、環境を時系列ベースの実行スタック(その後解放される部分)とは別のメモリ領域に格納する必要があることを意味し、ひいては、何らかの自由動的メモリ割り当てを意味します。そのため、多くの古い Algol ベースの言語(またはその方言)では、非ローカル変数にアクセスするローカル関数を戻り値として渡すことを許可していません。あるいは、関数を戻り値として渡すことを全く許可していませんが、そのような関数を引数として渡すことはまだ可能です。
GCCの C 言語におけるネストされた関数の実装は、実行不可スタック(NX スタック)の喪失を引き起こします。そのため、セキュア開発ライフサイクルを使用して設計されたソフトウェアでは、GCC のネストされた関数の使用が許可されないことがよくあります。[ 15 ] デフォルトでは、生成されたオブジェクト ファイルが実行可能スタックを必要とする場合、GCC は簡単な通知を発行します。C コードをコンパイルするときに明確な警告を表示するには、‑Wtrampolinesオプションを使用します。
この問題は、GCC がネストされた関数にジャンプするために「トランポリン」(スタック上の実行可能コード)を使用するために発生します。[ 16 ] GCC プロジェクトには、実行不可能な記述子を介してネストされた関数を提供する代替方法がありますが、これは現在 GCC のAdaコンパイラでのみ使用されています。