コンピュータサイエンスでは、プログラミング言語が関数を第一級の市民として扱う場合、その言語は第一級関数を持つと言われます。これは、その言語が関数を他の関数への引数として渡したり、他の関数から値として返したり、変数に代入したり、データ構造に格納したりすることをサポートすることを意味します。[ 1 ]プログラミング言語の理論家の中には、匿名関数(関数リテラル)のサポートも必要とする人もいます。[ 2 ]第一級関数を持つ言語では、関数の名前は特別な地位を持たず、関数型を持つ通常の変数として扱われます。[ 3 ]この用語は、1960年代半ばにクリストファー・ストラッチーが「第一級の市民としての関数」という文脈で造語しました。 [ 4 ]
関数型プログラミングスタイルでは、高階関数の使用が標準的な手法であり、第一級関数は不可欠です。高階関数の簡単な例として、map関数があります。map関数は、関数とリストを引数として受け取り、リストの各要素に関数を適用して生成されたリストを返します。map関数をサポートする言語は、関数を引数として渡す機能をサポートしている必要があります。
関数を引数として渡したり、結果として返したりする際には、特にネストされた関数や匿名関数で導入された非ローカル変数がある場合、実装上の困難がいくつかある。歴史的には、これらはfunarg 問題と呼ばれており、その名前はfunction argumentに由来する。[ 5 ]初期の命令型言語では、関数を結果型としてサポートしない ( ALGOL 60、Pascalなど) か、ネストされた関数、ひいては非ローカル変数を省略する ( Cなど) ことで、これらの問題を回避していた。初期の関数型言語Lisp は動的スコープのアプローチを採用しており、非ローカル変数は、定義された場所ではなく、関数が実行される時点でその変数の最も近い定義を参照する。Scheme では、字句スコープの第一級関数を適切にサポートする機能が導入され、関数への参照を裸の関数ポインタではなくクロージャとして扱う必要がある。[ 4 ]その結果、ガベージコレクションが必須となる。
このセクションでは、関数が第一級である関数型言語( Haskell)と、関数が第二級である命令型言語(C )で、特定のプログラミングイディオムがどのように扱われるかを比較します。
関数が第一級オブジェクトとして扱われる言語では、関数は他の値と同様に他の関数への引数として渡すことができます(別の関数を引数として受け取る関数は高階関数と呼ばれます)。Haskell 言語では次のようになります。
map :: ( a -> b ) -> [ a ] -> [ b ] map f [] = [] map f ( x : xs ) = f x : map f xs関数が第一級オブジェクトではない言語でも、関数ポインタやデリゲートなどの機能を使用することで、高階関数を記述できる場合が多い。C言語では、
void map ( int ( * f )( int ), int x [], size_t n ) { for ( int i = 0 ; i < n ; ++ i ) { x [ i ] = f ( x [ i ]); } }2 つのアプローチには、第一級関数のサポートとは直接関係のないいくつかの違いがあります。Haskell のサンプルはリストを操作しますが、C のサンプルは配列を操作します。どちらもそれぞれの言語で最も自然な複合データ構造であり、C のサンプルをリンク リストで操作するようにすると、不必要に複雑になります。これは、C 関数が追加のパラメータ (配列のサイズを指定する) を必要とする理由でもあります。C 関数は配列をその場で更新し、値を返しませんが、Haskell ではデータ構造は永続的です(古いリストはそのままに、新しいリストが返されます)。Haskell のサンプルは再帰を使用してリストを走査しますが、C のサンプルは反復を使用します。これもまた、両方の言語でこの関数を表現する最も自然な方法ですが、Haskell のサンプルはフォールドで、 C のサンプルは再帰で簡単に表現できました。最後に、Haskell 関数は多相型を持っていますが、これは C ではサポートされていないため、すべての型変数を型定数に固定しましたint。
匿名関数をサポートする言語では、そのような関数を高階関数の引数として渡すことができます。
main = map ( \ x -> 3 * x + 1 ) [ 1 , 2 , 3 , 4 , 5 ]匿名関数をサポートしていない言語では、代わりに名前をバインドする必要があります。
int f ( int x ) { return 3 * x + 1 ; }int main () { int list [] = { 1 , 2 , 3 , 4 , 5 }; map ( f , list , 5 ); }匿名関数やネストされた関数があると、それらが関数本体の外にある変数(非ローカル変数と呼ばれる)を参照することが自然になります。
main = let a = 3 b = 1 in map ( \ x -> a * x + b ) [ 1 , 2 , 3 , 4 , 5 ]関数を関数ポインタのみで表現する場合、関数本体の外にある値をどのように関数に渡すべきかが分からなくなるため、クロージャを手動で構築する必要が生じます。したがって、ここでは「第一級」関数という概念は適用できません。
typedef struct { int ( * f )( int , int , int ); int a ; int b ; } Closure ;void map ( Closure * closure , int x [], size_t n ) { for ( int i = 0 ; i < n ; ++ i ) { x [ i ] = ( closure -> f )( closure -> a , closure -> b , x [ i ]); } }int f ( int a , int b , int x ) { return a * x + b ; }void main () { int l [] = { 1 , 2 , 3 , 4 , 5 }; int a = 3 ; int b = 1 ; Closure closure = { f , a , b }; map ( & closure , l , 5 ); }また、 は、環境外のmap2 つの を参照する関数に特化していることに注意してくださいint。これはより一般的に設定できますが、より多くの定型コードが必要になります。がネストされた関数fであったとしても、同じ問題に遭遇したでしょう。これが、C でサポートされていない理由です。[ 6 ]
関数を返すとき、実際にはそのクロージャを返しています。C言語の例では、クロージャによってキャプチャされたローカル変数は、クロージャを構築する関数から戻るとスコープ外になります。後からクロージャを強制的に適用すると、未定義の動作が発生し、スタックが破損する可能性があります。これは「上向き引数問題」として知られています。
関数を変数に割り当てて(グローバル)データ構造内に格納することは、関数を返す場合と同様の困難を伴う可能性がある。
f :: [[ Integer ] -> [ Integer ]] f = let a = 3 b = 1 in [ map ( \ x -> a * x + b ), map ( \ x -> b * x + a )]ほとんどのリテラルと値の等価性をテストできるため、プログラミング言語が関数の等価性テストをサポートできるかどうかを問うのは自然なことです。さらに詳しく調べると、この問題はより難しく、いくつかの種類の関数の等価性を区別する必要があるようです。[ 7 ]
型理論では、型Aの値を受け取り、型Bの値を返す関数の型は、A → BまたはB Aと表記されます。Curry –Howard 対応では、関数型は論理的含意と関連付けられ、ラムダ抽象化は仮説的仮定の解除に対応し、関数適用はモーダス・ポネンス推論規則に対応します。型理論では、通常のプログラミング関数の他に、第一級関数を使用して連想配列や同様のデータ構造をモデル化することもできます。
プログラミングの圏論的説明において、第一級関数の利用可能性は閉圏の仮定に対応する。例えば、単純型付きラムダ計算は、デカルト閉圏の内部言語に対応する。
Erlang、Scheme、ML、Haskell、F#、Scalaなどの関数型プログラミング言語は、いずれも第一級関数を備えています。初期の関数型言語の一つであるLispが設計された当時は、第一級関数のすべての側面が十分に理解されていなかったため、関数は動的スコープで定義されていました。後のSchemeやCommon Lispの方言では、字句スコープの第一級関数が採用されています。
Perl、Python、PHP、Lua、Tcl /Tk、JavaScript、Ioなど、多くのスクリプト言語には第一級関数があります。
命令型言語においては、Algolと、Pascal、伝統的なC言語ファミリー、そして現代のガベージコレクションを採用した派生言語といった、Algolの派生言語とを区別する必要がある。Algolファミリーは、関数を引数として受け取るネストされた関数や高階関数は許容しているが、関数を結果として返す高階関数は許容していない(ただし、Algol 68は例外で、これは許容している)。その理由は、ネストされた関数が結果として返された場合に、非ローカル変数をどのように扱うべきかが不明であったためである(そして、Algol 68では、そのような場合に実行時エラーが発生する)。
C言語系では、関数を引数として渡すことも、関数を結果として返すこともできましたが、ネストされた関数をサポートしないことで問題を回避していました。(gccコンパイラは拡張機能としてネストされた関数をサポートしています。)関数を返すことの有用性は、主にトップレベル関数ではなく、非ローカル変数をキャプチャしたネストされた関数を返すことができる点にあるため、これらの言語は一般的に第一級関数を持つ言語とはみなされていません。
現代の命令型言語は多くの場合ガベージコレクションをサポートしており、第一級関数の実装が可能になっています。第一級関数は、C# 2.0 や Apple の C、C++、Objective-C 用 Blocks 拡張機能など、言語の後のバージョンでのみサポートされることが多かったのです。C++11 では匿名関数とクロージャのサポートが追加されましたが、この言語はガベージコレクションを行わないため、関数内で非ローカル変数を結果として返す際には特別な注意が必要です (下記参照)。
function関数を値として取得するには、特別な演算子を使用する必要があります(function foo)。 は関数オブジェクトに評価されます。#'fooは省略記法として存在します。このような関数オブジェクトを適用するには、funcall関数を使用する必要があります(funcall #'foo bar baz)。functools.partialバージョン2.5以降、およびバージョン2.6以降で明示的な部分適用が可能ですoperator.methodcaller。Method第Proc一級データとして使用する必要があります。このような関数オブジェクトを呼び出す構文は、通常のメソッドを呼び出す構文とは異なります。。{{cite journal}}: CS1 maint: bot: 元の URL の状態が不明です (リンク) (2010-02-16 にも記載)