第一級関数と第一級オブジェクト|具体例と両者の違い【関数型プログラミング】

第一級オブジェクト

第一級オブジェクト(first-class object、ファーストクラスオブジェクト)とは、無名のリテラルとして表現可能な値でかつ、変数への格納や引数や戻り値として受け渡しが行えるようなオブジェクトのことを言います。

一般的な整数型のリテラル値(0, 99, -1)や論理型の値(true, false)、文字列リテラルの値("abc")、配列リテラル([1, 2])、オブジェクトリテラル{key: 'val'}などが、この第一級オブジェクトに該当します。

言語によっては関数もリテラルの形で表現できる場合があり(function () {}等の無名関数やラムダ式)、またそれらの値は引数や戻り値として受け渡すことが可能となっています。この性質は「# 第一級関数」と呼ばれており、その場合の関数も第一級オブジェクトに相当します。

第一級オブジェクトの例

9              // 数値リテラル
true           // 論理値リテラル
"str"          // 文字列リテラル
'c'            // 文字リテラル(C++言語で利用)
[1, 2]         // 配列リテラル(JavaScriptやスクリプト言語で利用)
{k: 'v'}       // オブジェクトリテラル、連想配列/辞書/マップ・リテラル
function () {} // 関数リテラル
(lambda () T)  // ラムダ式(Lisp)
[]() {}        // ラムダ式(C++)

第一級オブジェクトの性質

9, "", [], {}; // 無名のリテラルとして表現が可能
var v = 9;     // 変数への格納が可能
alert(9);      // 引数として渡すことが可能
return [];     // 戻り値として返すことが可能
{k: [9, ""]};  // データ構造への格納が可能
[] == 0;       // 等値性の比較が可能
[1, 2][0];     // 変数名と独立した形で存在することが可能
[v, 9];        // 実行時に動的に生成することが可能

このように、計算要素として制限なく自由に扱えるオブジェクトのことを「第一級」オブジェクトと呼びます。なお、より具体的な性質は以下の通りです。

無名のリテラルとして表現可能である。
変数に格納可能である。
データ構造に格納可能である。
それ自体が独自に存在できる(名前とは独立している)。
他のものとの等値性の比較が可能である。
プロシージャや関数のパラメータとして渡すことができる。
プロシージャや関数の戻り値として返すことができる。
実行時に構築可能である。
表示可能である。
読み込むことができる。
分散したプロセス間で転送することができる。
実行中のプロセスの外に保存することができる。

実行時に任意の箇所で値やオブジェクトの生成が行える「配列リテラル」や「無名関数」も、この第一級オブジェクトに該当します。

// 無名関数を代入
var f = function (ary) { console.log(ary); }
// 配列リテラルを実引数として渡す
f([1, 2]);

// 無名のリテラルに対する即時評価も可能
(function (n) {})([].length);

C言語の配列と関数は第一級オブジェクトではない

C言語の「配列」については、データ構造への格納や、戻り値としての受け渡しが行えないため、第一級オブジェクトとは見なされません。

int a[] = {1, 2};
int b[] = a;      // データ構造への格納が行えない
return a;         // 配列型は戻り値に出来ない

// 引数として渡すことはできるが、あくまでポインタ渡しとなる
char s[] = {'a', 'b', '\0'};
f(s); // void f(char s[]) {} // 仮引数側でも実体はポインタ型として定義される

// 複合リテラルの場合はポインタ変数による受け取りが必要
// あくまで参照であり、元オブジェクトの生存期間に依存する
int *a = (int []){1, 2, argc};
参考: 【C言語】配列は戻り値にできない【配列を適切に返す方法】

第一級オブジェクトへの対応

C言語の「関数」についても、実行時に任意のタイミングで生成することができないため、第一級オブジェクトとはみなされません。ただし、C言語の配列も関数も、ポインタ型を活用することで、第一級オブジェクトの性質の多くを実現することが可能となっています。

char a[] = {'c', '\0'};
char *b = a;     // ポインタ変数として格納
printf("%p", b); // ポインタとして渡す

// 関数ポインタとして保持(引数や戻り値としてやり取りすることも可能)
int (*p)(const char *) = puts;

なおC++のラムダ式やC言語拡張のBlocksについては、第一級オブジェクトとして扱うことができます。いずれもクロージャを扱うことが可能となっています。

// ラムダ式(C++)
auto lambda = [](int v) { printf("%d", v); };
// Blocks(C言語拡張)
void (^blocks)(int) = ^(int v) { printf("%d", v); };

またプログラミング言語によっては「関数オブジェクト」を用いることで、第一級オブジェクトや第一級関数の性質を実現することが可能となっています。関数オブジェクトはファンクタや無名インナークラス、インターフェースの仕組みを活用して実現されることがあります。

参考: 関数オブジェクト【ファンクタ】

第一級関数

第一級関数(first-class function、ファーストクラスファンクション)は関数を第一級オブジェクトとして扱うことのできる性質、またはその関数のことを言います。第一級関数をサポートしたプログラミング言語の多くは、関数をリテラルとして表現でき、またそれらの関数を引数の値や戻り値として受け渡すことが可能となっています。現代では関数型言語(Lisp, Haskell)以外にも、C++やC#, Java, JavaScript, PHP, Perl, Ruby, Python等の様々な言語がこの第一級関数と同等の性質を取り入れています。

関数型プログラミングでよく見かけるfor_each([1, 2], print);式におけるprint関数オブジェクトや、map, filter, reduce等の高階関数活用時([1, 2].map(function (v) { return v + 1 }))における無名関数function (v) { return v + 1 }の利用方は、この第一級関数の活用例に相当します。

第一級関数の性質・活用例

JavaScriptの関数や無名関数は第一級関数として扱うことができます。

function print(v) { console.log(v); } // print関数

// 変数への格納が可能
var p = print;

// データ構造への格納が可能
var ary = [p, print];

// 引数として渡すことが可能
[1, 2].forEach(print);

// 無名のリテラルとして表現が可能
[1, 2].forEach(function (v) { console.log(v); });

// 戻り値として返すことが可能
function print_lazy(lazy) {
  return function () { console.log(lazy); };
}

var job = print_lazy("hello")
job() // "hello"

var jobs = ["hello", "world"].map(print_lazy)
jobs[1]() // "world"
jobs.forEach(f => f()) // "hello", "world"

ちなみにforEachメソッドやfor_each関数は、配列の要素を順に第一級関数へと適用していく機能です。mapメソッドはその適用結果を新たな配列の要素として返す機能です。参考までに、for_each関数の内部では以下のような処理が行われています。

function for_each(_array, _function) {
  for (var i = 0; i < _array.length; i++) {
    _function(_array[i]);
  }
}
var print = function (v) { console.log(v); };
for_each([1, 2], print);
// 配列の値`1`と`2`が順に`print`関数の実引数として渡されてゆく。
// つまり`for_each([1, 2], print)`は、
//    print(1);
//    print(2);
// と同じ動作をする。
// 無名関数を直接渡すことも可能
for_each([1, 2], function (v) { console.log(v); });
// 更に`console.log`を渡すことも可能
for_each([1, 2], console.log);

関数型プログラミング

for_each関数の例のように、第一級関数の性質を用いて一定の処理を抽象化するような技法は「関数型プログラミング」とも呼ばれています。for_each関数とprint関数という異なる式を組み合わせることによって、目的の処理を実現する、これがまさに関数型プログラミングの発想です。

汎用的な枠組みを提供するfor_each関数と、ユーザ独自の挙動を具体化するprint関数、その両者の連携によって目的の処理を実現するわけです。第一級関数は関数型プログラミングを実現する上で最も重要な概念となっています。

広告

広告