コンピュータ プログラミングにおいて、ネストされた関数(またはネストされたプロシージャやサブルーチン) は、別の囲みブロック内で定義され、囲みブロック内でレキシカル スコープを持つ名前付き 関数です。つまり、囲みブロックの本体内でのみ名前で呼び出し可能であり、外部関数を含む外部ブロックで宣言された識別子を使用できます。囲みブロックは通常は別の関数ですが、常にそうであるとは限りません。
ネストされた関数に対するプログラミング言語のサポートはさまざまです。構造化プログラミング言語に関しては、ALGOL、Simula 67、Pascalなどの一部の古い言語と、一般的に使用されているJavaScriptでサポートされています。動的言語と関数型言語では一般的にサポートされています。ただし、標準CやC++などの一般的に使用されている言語ではサポートされていません。
他のプログラミング テクノロジでも同様の利点が得られます。たとえば、ラムダ関数を使用すると、関数を関数内 (または他の場所) で定義でき、同様のデータの非表示とカプセル化が可能になります。特に、ラムダ関数には名前がなく (匿名)、名前で呼び出すことができず、可視性もありません。
属性
ネストされた関数のスコープは、関数ブロックまたは関数本体内のブロックなど、その関数を含むブロックです。ネストされた関数は、その関数を含むブロックの外部からは見えません (名前で呼び出すことはできません)。
ネストされた関数は、同じ名前の内部宣言によってマスクされている場合を除き、任意の囲みブロックで宣言された 識別子(つまり、関数、変数、型、クラスの名前) を使用できます。
ネストされた関数は、ネストされた関数内で再帰的に宣言され、深くネストされた構造を形成できます。深くネストされた関数は、囲んでいる関数を含む、囲んでいるすべてのブロックで宣言された識別子にアクセスできます。
ネストされた関数は、特定の状況では、クロージャの作成につながる場合があります。ネストされた関数が囲んでいる関数から脱出できる場合、たとえば関数がファーストクラスオブジェクトであり、ネストされた関数が別の関数に渡されるか、囲んでいる関数から返される場合、クロージャが作成され、この関数の呼び出しは元の関数の環境にアクセスできます。直接囲んでいる関数のフレームは、最後の参照クロージャが終了するまで存続する必要があり、したがって、クロージャで参照される非ローカル 自動変数は、囲んでいるブロックの存続期間を超えてクロージャが存続することを許可する言語ではスタックに割り当てることができません。これはfunarg 問題として知られており、ネストされた関数がいくつかのより単純な言語で実装されなかった主な理由です。これは、関数がさまざまなレベルにネストされ、環境のさまざまな部分を共有する場合に特に、コードの生成と分析が非常に複雑になるためです。
価値
ネストされた関数テクノロジにより、プログラマーは、情報の隠蔽、カプセル化、分解などの有益な属性を含むソース コードを記述できます。プログラマーは、タスクを、タスクのコンテキスト内でのみ意味を持つサブタスクに分割して、サブタスク関数が、それらを使用するように設計されていない呼び出し元から隠されるようにすることができます。
ブロックスコープにより、関数はパラメータを渡したりグローバル変数を使用したりせずに、囲んでいるブロック(囲んでいる関数を含む)の状態を共有できます。[1]
用途
ヘルパー
ネストされた関数は通常、ヘルパー関数または再帰関数として機能します(上記のクイックソートの例のように)。
制御フロー
ネストされた関数は、一般的な非構造化制御フローの return ステートメントを使用することで、非構造化制御フローに使用できます。これは、言語の他の組み込み機能で可能なものよりもきめ細かい制御に使用できます。たとえば、が利用できない場合は for ループを早期に終了したり、マルチレベルまたは例外が利用できない場合はbreakネストされたfor ループbreakを早期に終了したりできます。
高階関数
いくつかの言語では、外側の関数から一連のパラメータにアクセスするネストされた関数(つまりクロージャ)を作成し、その関数を外側の関数の戻り値にすることができます。したがって、追加のパラメータをほとんどまたはまったく与えずに、特定のタスクを実行するように設定された関数を返すことができ、パフォーマンスを大幅に向上させることができます。[2]
例
簡単な例
Pascal での簡単な例:
関数E ( x :実数) :実数;関数F ( y :実数) :実数;開始F := x + y終了;開始E := F ( 3 ) + F ( 4 )終了;
関数はF内にネストされていますE。Eのパラメータは(は の一部であるため)x内でも可視ですが、 と はそれぞれ と の外部では不可視であることに注意してください。
FFExyEF
同様に、Standard MLでは次のようになります。
fun e ( x : real ) =
let
fun f y = x + y
in
f 3 + f 4
end ;
Haskellの場合:
e :: Float -> Float e x = f 3 + f 4ただし、f y = x + y
PL/Iの場合:
e: プロシージャ(x)は(float)を返します。
x を float として宣言します。
f: 手続き(y)は(float)を返します。
y float を宣言します。
x + yを返す
終わり;
f(3.0) + f(4.0) を返します。
終わり;
Pythonの場合:
def e ( x : float ) -> float :
def f ( y : float ) -> float :
戻り値 x + y
戻り値 f ( 3.0 ) + f ( 4.0 )
GNU C [3]では、標準Cをネストされた関数で拡張しています。
フロートE (フロートx ) {フロートF (フロートy ) {戻り値x + y ; }戻り値F ( 3 ) + F ( 4 ); }
クイックソート
以下はクイックソートの実装である: [4]
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 ], 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 パーティション ベースのクイックソートの実装です。
テンプレート< typename RandomAccessIterator > auto Sort ( RandomAccessIterator Begin , RandomAccessIterator End ) -> void { auto Partition = [ & ]() { //Hoare パーティション スキームauto & Pivot = * Begin ; auto ForwardCursor = Begin ; auto BackwardCursor = End - 1 ; auto PartitionPositionFound = false ; auto LocatePartitionPosition = [ & ]() { while ( * ForwardCursor < Pivot ) ++ ForwardCursor ; while ( Pivot < * BackwardCursor ) -- BackwardCursor ; if ( ForwardCursor >= BackwardCursor ) PartitionPositionFound = true ; else Swap ( * ForwardCursor , * BackwardCursor ); }; //簡単なヘルパー関数auto MoveOnAndTryAgain = [ & ]() { ++ ForwardCursor ; -- BackwardCursor ; }; //実際のパーティション処理の簡単な概要while ( true ) { LocatePartitionPosition (); if ( PartitionPositionFound ) return BackwardCursor + 1 ; else MoveOnAndTryAgain (); } }; //クイックソートアルゴリズムの簡単な概要if ( Begin < End - 1 ) { auto PartitionPosition = Partition (); Sort ( Begin , PartitionPosition ); Sort ( PartitionPosition , End ); } }
言語
ネストされた関数をサポートする注目すべき言語は次のとおりです。
- ALGOL 68、Simula、Pascal、Modula-2、Modula-3、Oberon、PL/I、Seed7、AdaなどのALGOLベースの言語
- SchemeやCommon LispなどのLispの最新バージョン(レキシカルスコープ付き)
- ECMAScript ( JavaScriptとActionScript )
- ダーツ[5]
- Kotlin(ローカル関数[6])
- さび
- Scala(ネストされた関数[7])
- Ruby、Python、Lua、PHP、Perlなどのスクリプト言語のさまざまなレベルのサポート
- GCCは言語拡張としてC言語のネストされた関数をサポートしています。[8]
- C#、C# 7.0 以降
- D言語は、ネストされた関数を持つ C 関連言語です。
- Fortran は、 Fortran-90以降、単一レベルのネストされた ( CONTAINed ) サブルーチンと関数をサポートしています。
- MATLAB (フルサポート)
- ウルフラム言語
- Golang(関数クロージャ[9])
関数型言語
Scheme などのほとんどの関数型プログラミング言語では、ネストされた関数はループを含むアルゴリズムを実装する一般的な方法です。単純な (末尾の)再帰内部関数が作成され、これがアルゴリズムのメイン ループとして動作し、外部関数は 1 回だけ実行する必要があるスタートアップ アクションを実行します。より複雑なケースでは、複数の相互再帰関数が内部関数として作成されることがあります。
代替案
ネストされた関数を使用した場合と同様のプログラミング結果を実現するために、さまざまな代替手法を使用できます。
モジュール性
一般的な代替手段は、言語のモジュール化テクノロジを活用することです。一部の関数はモジュールの外部で使用するために公開され、一部はモジュール内でのみ表示されます。
C言語では、関数と変数をstaticとして宣言してファイル外のコードから隠すことでこれを実装できます。 [10]これにより、データの隠蔽、カプセル化、分解が可能になりますが、ネストされた関数とは異なるレベルの粒度になります。このモジュール性は、1レベル以上のネストをサポートしていません。
オブジェクト指向言語では、クラスは通常、関数と状態をクラスの消費者からは隠蔽しつつ、クラス内からアクセス可能なスコープを提供します。一部の言語では、クラスをネストできます。
パラメータ
データ隠蔽を実装するために、関数は共有データをパラメータとして渡すことができますが、これにより関数呼び出しの複雑さが増します。[1]
C言語では、共有データを含む構造体へのポインタを渡すことによって実装されるのが一般的です。[10]
ラムダ
PHPや他の言語では、ラムダが代替手段となります。関数は、通常の関数構文で宣言されるのではなく、コード ステートメントで定義されます。関数には名前はありませんが、関数参照を介して呼び出すことができます。このような関数は、関数内だけでなく他のスコープでも定義できます。匿名関数でローカル変数を使用するには、クロージャを使用します。
言語別の代替案
次の言語は、ネストされた関数に似た機能を提供します。
- C++ – クラスは同様のデータの隠蔽とカプセル化を可能にします。クラス内でクラスを定義すると同様の構造が提供されます ( C++ の関数オブジェクトを参照)
- C++11以降 – ラムダ式経由(上記のクイックソートの例を参照)[11]
- Eiffel – 言語をシンプルに保つためにルーチンのネストを明示的に禁止します。ただし、(値を返す)関数の結果を表すために、特別な変数Resultを使用する規則は許可します。
- C#とVisual Basic – ラムダ式経由
実装
ネストされた関数の実装は、非ローカル変数を参照するネストされた関数への参照がクロージャを作成するため、見た目以上に複雑になることがあります。このため、ネストされた関数は、コンパイラの実装を困難にするため、C、C++、Javaなどの一部の言語ではサポートされていません。[10] [13]ただし、一部のコンパイラは、コンパイラ固有の拡張機能としてサポートしています。よく知られている例としては、 CのGNU C実装があり、これはPascal、Ada、Modulaなどの言語のコンパイラとコードを共有しています。
非ローカルオブジェクトへのアクセス
レキシカルスコープ言語でネストされたプロシージャを実装する方法はいくつかありますが、標準的な方法は次のとおりです。
- 非ローカル オブジェクトX には、マシン スタックのアクティベーション フレームのアクセス リンクを介してアクセスします。呼び出し元 C は、呼び出し自体の前に、呼び出されたプロシージャ Pの直接の語彙カプセル化 (P) の最新のアクティベーションへの直接リンクをプッシュすることで、呼び出されたプロシージャ P を支援します。その後、P は、固定数(P.depth - X.depth) のリンク (通常は少数)をたどることで、特定の X の適切なアクティベーションをすばやく見つけることができます。
- 呼び出し側は、C.depth - P.depth + 1 の古いリンクをたどって (それ自体で) この直接リンクを作成し、(P) の最新のアクティベーションに至り、次にこれらのリンクを一時的にそのアクティベーションへの直接リンクでブリッジします。このリンクは後に P とともに消え、その下の古いリンクが再び使用される可能性があります。
- (P) = C / (C) / ((C)) / 等の場合は、P は C に対して可視であり、したがって C から呼び出すことができることに注意してください。
このオリジナルの方法は見た目よりも高速ですが、実際の最新のコンパイラでは(ディスプレイや同様の技術を使用して)最適化されることがよくあります。
一部のコンパイラで使用されるネストされた関数を実装する別の方法は、コンパイルの中間段階で ラムダ リフティングと呼ばれるプロセスを使用して、ネストされた関数をネストされていない関数に変換 (「リフト」) することです (追加の非表示のパラメーターがアクセス リンクを置き換えます)。
値としての機能
レキシカルスコープの非ローカルを持つ ローカル関数が結果として渡されるためには、言語ランタイムコードは、関数がカプセル化する関数内で参照する環境(データ)も暗黙的に渡す必要があります。これにより、囲む関数の現在のアクティベーションが存在しなくなった場合でも、環境に到達できるようになります。 [14]これは、環境が時系列ベースの実行スタック(の後で再利用される部分)とは別のメモリ領域に格納される必要があることを意味し、これは、ある種の自由に動的なメモリ割り当てを意味します。そのため、多くの古い Algol ベースの言語(またはその方言)では、非ローカルにアクセスするローカル関数を戻り値として渡すことは許可されていません。または、そのような関数を引数として渡すことはまだ可能ですが、関数を戻り値として渡すことをまったく許可していません。
実行不可スタック
GCCのネストされた関数の実装により、実行不可 スタック(NX スタック) が失われます。この実装では、実行時にマシン スタックに置かれたジャンプ命令を通じてネストされた関数を呼び出します。これには、スタックが実行可能であることが必要です。したがって、GCC では、実行不可スタックとネストされた関数は相互に排他的です。プログラムの開発でネストされた関数が使用されると、GCC が‑Wtrampoline状態を警告するオプション付きで呼び出されない限り、NX スタックは暗黙的に失われます。セキュア開発ライフサイクルを使用して設計されたソフトウェアでは、NX スタックが失われるため、この特定のコンパイラではネストされた関数の使用が許可されないことがよくあります。[15]
参照
参考文献
- ^ ブライト 2004より。
- ^ 高階関数とラムダ - Kotlin プログラミング言語
- ^ Rothwell, Trevis J. (2011). GNU C リファレンスマニュアル. Free Software Foundation, Inc. p. 63.
- ^ Re: ネスト関数 - なぜ? [usurped]、baavgai [usurped]、2012 年 1 月 14 日
- ^ 「Dart 言語のツアー」。
- ^ 「関数 | Kotlin」。
- ^ 「ネストされたメソッド」。
- ^ 「ネストされた関数 - GNU コンパイラ コレクション (GCC) の使用」。GNU プロジェクト。2007年 1 月 6 日閲覧。
- ^ 「囲碁ツアー」
- ^ abc 「質問 20.24: C にはネストされた関数がないのはなぜですか?、comp.lang.c FAQ
- ^ 「ネストされた関数 - Rosetta コード」。
- ^ 「ネストされた関数 - Rosetta コード」。
- ^ Dave Vandervies による回答、2009 年 8 月 28 日 17:45、「ネストされた関数が C 標準でサポートされていないのはなぜですか?」
- ^ このような関数コードとその環境の組み合わせは、クロージャと呼ばれることもあります。
- ^ Walton, Jeffrey. 「C ベースのツールチェーンの強化」。The Open Web Application Security Project (OWASP) 。2017 年2 月 28 日閲覧。
- Bright, Walter (2004 年 5 月 1 日)。「ネストされた関数」。Dr . Dobb's。
外部リンク
- comp.lang.c FAQ: ネストされた関数
- 「6.4 ネストされたプロシージャと関数」。FreePascal ドキュメント。
