はじめまして。エンジニアをしています、鷲見と申します。
今回は高階関数とラムダ式について書いてみようと思います。
高階関数とは
高階関数(こうかいかんすう,higher-order function)とは引数に関数を指定できたり、
戻り値として関数を返せる関数で、関数型言語などで使用されます※1。
高階関数を使用するメリットは、単純な関数を組み合わせることにより、
柔軟性の高いコードを書くことが出来るという点です。
高階関数の例として関数型言語Schemeのmap関数を見てみます。
map関数は、指定したリストのそれぞれの要素に対して同じ関数を適用する関数です。
例えばリストの要素を全て2乗したい場合は以下のように書くことができます。
; xの2乗を返す関数を定義 (define (square x) (* x x)) ; リストの要素(1から5までの数値)を2乗する (map square '(1 2 3 4 5))
結果
(1 4 9 16 25)
Schemeはラムダ式が使えますから、以下のように書くこともできます。
; リストの要素それぞれを2乗する(ラムダ式を使用した場合) (map (lambda (x) (* x x)) '(1 2 3 4 5))
結果
(1 4 9 16 25)
このように高階関数はラムダ式と組み合わせることで、より効果を発揮します。
Schemeは前置記法で記述するので違和感を感じる方もいるかもしれませんが、
map関数の有効性はなんとなくわかっていただけるのではないでしょうか?
高階関数やラムダ式はRubyやPythonなどの関数型言語の影響を受けているプログラム言語では
当たり前のように使えるわけですが、ではCから派生した言語ではどうなのでしょうか?
実際に見てみましょう。というのがこの記事の趣旨です。
具体的にはSchemeのmap関数とほぼ同等の機能を持つ関数(メソッド)を
C,C++,Objective-C,Javaのそれぞれで実装します。
簡単のためmap関数はすべて整数型の配列(ベクタ,リスト)に対して関数を適用するものとし,
シグネチャはできるだけSchemeのmap関数と同じような形式にしています。
C(ANSI-C)で高階関数を使ってみる
Cで同じようなことをする場合は関数ポインタを使用することになります。
Schemeのmap関数と同様の機能をもつ関数を実装する場合は以下のようなコードを書く必要があります。
mapの定義
/** * 引数の値を2乗して返す */ int square(int n) { return n * n; } /** * 配列の要素すべてをfuncで指定された関数の引数として渡し、結果をresultに格納する * @param[in] source 入力配列 * @param[out] result 出力配列 * @param[in] n 配列の要素数 * @param[in] func 適用する関数 */ void map(const int *source, int *result, size_t n, int (*func)(int)) { unsigned int i; for (i = 0; i < n; i++) { result[i] = func(source[i]); } }
mapの実行
// 入力データをセット int numbers[] = {1, 2, 3, 4, 5}; int result[5] = {0}; unsigned int i; // map関数を実行 map(numbers, result, 5, (int (*)(int))square); // 結果を表示 for (i = 0; i < 5; i++) { printf("%d ", result[i]); }
結果
1 4 9 16 25
ネイティブのCにはラムダ式に該当する機能が存在しないので、引数に指定する関数をあらかじめ定義しておく必要があります。
C++で高階関数を使ってみる
2011年に標準化されたC++11(C++0x)でラムダ式がサポートされました。
C++のラムダ式の基本的な記述方法は以下の通りです。
// int型の変数2つを引数にとるラムダ式 [](int x, int y) { return x + y; // 戻り値の型は型推論により省略可能 }
C++のラムダ式がユニークなのは[]の中にラムダでアクセスする外部変数のオプションなどを記述(キャプチャ)する点で、例えば以下のように書くことができます。
int n = 10; [n](int x, int y) { return x + y + n; }
Schemeのmap関数と同様の機能をもつメソッドは以下のようなコードで実装可能です。
mapの定義
/** * 配列の要素すべてをfuncで指定された関数の引数として渡し、結果をresultに格納する * @param source 入力ベクタ * @param func 適用する関数 * @return 出力ベクタ */ template std::vector map(std::vector source, MAP func) { std::vector result; for (int i = 0; i < source.size(); i++) { result.push_back(func(source[i])); } return result; }
mapの実行
// 入力データをセット std::vector numbers; for (int i = 1; i <= 5; i++) { numbers.push_back(i); } // mapを実行(STLのmapではない) std::vectorresult = map(numbers, [](int n) -> int { return n * n; }); // 結果を表示 for (int i = 0; i < result.size(); i++) { std::cout << result[i] << " "; }
結果
1 4 9 16 25
上記の例では配列(ベクタ)の要素をすべて2乗していますが、例えば3乗するように変更したい場合は以下のようにするだけでOKです。
std::vectorresult = map(numbers, [](int n) -> int { return n * n * n; });
ちなみにC++にはSTLにfor_eachという関数がありますので
わざわざmap関数を作らなくても同じようなことができます。
std::vectorresult; std::for_each(numbers.begin(), numbers.end(), [&result] (int n) { result.push_back(n * n); });
Objective-Cで高階関数を使ってみる
Objective-Cではブロック構文(ブロック, Blocks)を使用することができます。
厳密にいえばブロック構文はObjective-Cの構文ではなく、Appleが提案している機能です。
実は環境がMacOSXv10.6以降、iOS4.0以降であればCでも使用可能なのですが、
MacアプリやiOSアプリをCでゴリゴリ開発することはあまりないと思うので、
ここではObjective-Cの機能としています※2。
Objective-Cのブロック構文の基本的な記述方法は以下の通りです。
// int型の変数2つを引数にとるブロック構文 ^(int x, int y) { return x + y; };
ブロック構文はC++のラムダ式とは違い、特に指定することなくレキシカルスコープの変数を参照することができます。
ただし、変更する場合はレキシカル変数に「__block」修飾子をつける必要があります。
int n = 10; ^(int x, int y) { return x + y + n; };
もっと詳しいことが知りたい方はAppleのブロックプログラミングトピックを参照されるとよいでしょう。
https://developer.apple.com/jp/devcenter/ios/library/documentation/Blocks.pdf
map関数と同様の機能をもつメソッドは以下のようなコードで実装可能です。
mapの定義
/** * 配列の要素すべてをfuncで指定されたメソッドの引数として渡し、結果を返す * @param array 入力配列 * @param func 適用する関数 * @return 出力配列 */ - (NSArray *)map:(NSArray *)array func:(int (^)(int))func { NSMutableArray *result = [NSMutableArray array]; for (NSNumber *num in array) { [result addObject:[NSNumber numberWithInt:func(num.intValue)]]; } return result; }
mapの実行
// 入力データをセット NSMutableArray *numbers = [NSMutableArray array]; for (int i = 1; i <= 5; i++) { [numbers addObject:[NSNumber numberWithInt:i]]; } // mapを実行 NSArray *result = [self map:numbers func:^(int n) { return n * n; }]; // 結果を表示 for (NSNumber *num in result) { NSLog(@"%d", num.intValue); }
結果
1 4 9 16 25
Javaで高階関数を使ってみる
2013年9月にリリースされるJava8で正式にラムダ式がサポートされることになりました。
Java8はこのブログを書いている時点ではまだ正式リリースがされていないため、
APIがころころ変更されているようですが、ラムダ式自体は以下のような構文で書くことができるようです。
// int型の変数2つを引数にとるラムダ式 (int x, int y) -> { x + y; } // 引数のないラムダ式 () -> 3.14
map関数と同様の機能をもつメソッドは以下のようなコードで実装可能です。
なお,以下はJDK 8 build b86で動作確認をしています。
mapの定義
private interface MAP {Integer apply(Integer i);}; /** * リストの要素すべてをfuncで指定された関数の引数として渡し、結果を返す * @param list 入力リスト * @param func 適用する関数 * @return 出力リスト */ private static List map(List list, MAP func) { List result = new ArrayList<>(); for (Integer li : list) { result.add(func.apply(li)); } return result; }
mapの実行
// 入力データをセット List numbers = new ArrayList<>(); for (int i = 1; i <= 5; i++) { numbers.add(i); } // mapを実行(java.util.MapのMapではない) List result = map(numbers, (Integer n) -> { return n * n; }); // 結果を表示 for (Integer li : result) { System.out.print(li + " "); }
結果
1 4 9 16 25
JavaについてはJava8がリリースされたらもう少し詳しく見てみたいなと思っています。
以上今回は4つの言語で高階関数を使ってみました。
上記のようにラムダ式やブロック構文を使用すると、関数ポインタを使用するよりも美しいコードを書くことができます。
近年多くのプログラム言語がラムダ式やクロージャをサポートするようになりましたので
初学者の方はマスターしてみてはいかがでしょうか?
※1 ちなみに、「高階」をキーボードで入力する場合は「こうかい」と打つよりも「たかしな」と打って変換した方が早いです。
※2 逆にいえばMacOSXv10.6以降、iOS4.0以降でなければObjective-Cでも使用できません。
おまけ.ブロック構文を用いたC言語での高階関数
mapの定義
void map_b(const int *source, int *result, size_t n, int (^func) (int)) { unsigned int i; for (i = 0; i < n; i++) { result[i] = func(source[i]); } }
mapの実行
// 入力データをセット int numbers[] = {1, 2, 3, 4, 5}; int result[5] = {0}; unsigned int i; // map関数を実行 map_b(numbers, result, 5, ^(int n) { return n * n; }); // 結果を表示 for (i = 0; i < 5; i++) { printf("%d ", result[i]); }