ホーム » スタッフ » 斉藤徹 » 講義録 » 情報構造論 » 関数ポインタとラムダ式

関数ポインタとラムダ式

関数ポインタとコールバック関数

JavaScript のプログラムで、以下のようなコーディングがよく使われる。このプログラムでは、3と4を加えた結果が出てくるが、関数の引数の中に関数宣言で使われるfunctionキーワードが出てきているが、この意味を正しく理解しているだろうか?

このような (function()…)は、無名関数と呼ばれている。(=>を使った書き方はアロー関数と呼ばれている) これは「関数を引数として渡す機能」と、「一度しか使わないような関数にいちいち名前を付けないで関数を使うための機能」であり、このような関数を引数で渡す機能はC言語では関数ポインタと呼ばれたり、新しいプログラム言語では一般的にラムダ式などと呼ばれる。

// JavaScriptの無名関数の例 3+4=7 を表示
console.log( (function( x , y ) {
                 return x + y ;
              })( 3 , 4 ) ) ; // 無名関数
console.log( ((x,y) => {
                 return x + y ;
              })( 3 , 4 ) ) ; // アロー関数(ラムダ式)

C言語の関数ポインタの仕組みを理解するために、以下のプログラムを示す。

int add( int x , int y ) {
   return x + y ;
}
int mul( int x , int y ) {
   return x * y ;
}
void main() {
   int (*f)( int , int ) ; // fは2つのintを引数とする関数へのポインタ
   f = add ;               // f = add( ... ) ; ではないことに注意
   printf( "%d¥n" , (*f)( 3 , 4 ) ) ; // 3+4=7
                 // f( 3 , 4 ) と書いてもいい
   f = mul ;
   printf( "%d¥n" , (*f)( 3 , 4 ) ) ; // 3*4=12
}

このプログラムでは、関数ポインタの変数 f を定義している。「 int (*f)( int , int ) ; 」 は、“int型の引数を2つ持つ、返り値がint型の関数”へのポインタであり、「 f = add ; 」では、f に加算する関数addを覚えている。add に実引数を渡す()がないことに注目。C言語であれば、関数ポインタ変数 f には、関数 add の機械語の先頭番地が代入される。

そして、「 (*f)( 3 , 4 ) ; 」により、実引数を3,4にて f の指し示す add を呼び出し、7 が答えとして求まる。

こういう、関数に「自分で作った関数ポインタ」を渡し、その相手側の関数の中で自分で作った関数を呼び出してもらうテクニックは、コールバックとも呼ばれる。コールバック関数を使うC言語の関数で分かり易い物は、クイックソートを行う qsort() 関数だろう。qsort 関数は、引数にデータを比較するための関数を渡すことで、様々な型のデータの並び替えができる。

#include <stdio.h>
#include <stdlib.h>

// 整数を比較するコールバック関数
int cmp_int( int* a , int* b ) {
   return *a - *b ;
}
// 実数を比較するコールバック関数
int cmp_double( double* a , double* b ) {
   double ans = *a - *b ;
   if ( ans == 0.0 )
      return 0 ;
   else if ( ans > 0.0 )
      return 1 ;
   else
      return -1 ;
}

// ソート対象の配列
int    array_int[ 5 ] = { 123 , 23 , 45 , 11 , 53 } ;
double array_double[ 4 ] = { 1.23 , 12.3 , 32.1 , 3.21 } ;

void main() {
   // 整数配列をソート
   qsort( array_int , 5 , sizeof( int ) ,
          (int(*)(const void*,const void*))cmp_int ) ;
   //     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~この分かりにくい型キャストが必要なのがC言語の面倒な所
   for( int i = 0 ; i < 5 ; i++ )
      printf( "%d\n" , array_int[ i ] ) ;
   // 実数配列をソート
   qsort( array_double , 4 , sizeof( double ) ,
          (int(*)(const void*,const void*))cmp_double ) ;
   //     ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
   for( int i = 0 ; i < 5 ; i++ )
      printf( "%f\n" , array_double[ i ] ) ;
}

無名関数

コールバック関数を使っていると、データを比較するだけの関数とか簡単な短い処理が使われることが多い。こういった処理を実際に使われる処理と離れた別の場所に記述すると、プログラムが読みづらくなる。この場合には、その場で関数の名前を持たない関数(無名関数)を使用する。(C++の無名関数機能は、最近のC++の文法なのでテストには出さない)

void main() {
   int (*f)( int , int ) ; // fは2つのintを引数とする関数へのポインタ
   f = []( int x , int y ) { return x + y ; } ; // add を無名関数化
   printf( "%d¥n" , (*f)( 3 , 4 ) ) ; // 3+4=7

   // mul を無名関数にしてすぐに呼び出す3*4=12 
   printf( "%d¥n" , []( int x , int y ) { return x * y ; }( 3 , 4 ) ) ;
   // メモ:C++11では、ラムダ式=関数オブジェクト
   //      C++14以降は、変数キャプチャなどの機能が追加されている。
}

C++の変数キャプチャとJavaScriptのクロージャ

JavaScript のクロージャ

JavaScriptにおいて、関数オブジェクトの中で、その周囲(レキシカル環境)の局所変数を参照できる機能をクロージャと呼ぶ。クロージャを使うことでグローバルな変数や関数の多用を押さえ、カプセル化ができることから、保守性が高まる。

// JavaScriptにおけるクロージャ
function foo() {
   let a = 12 ; // 局所変数
   console.log( (function( x , y ) {
                    return a + x + y ;  // 無名関数の外側の局所変数aを参照できる
                 })( 3 , 4 ) ) ;
}
foo() ;

C++の変数キャプチャ

C++でも無名関数などでクロージャと同様の処理を書くことができるようにするために変数キャプチャという機能がC++14以降で使うことができる。

// C++のラムダ関数における変数キャプチャ
void main() {
   int a = 12 ;
   printf( "%d\n" ,
           [a]( int x , int y ) {  // 変数キャプチャ[a]の部分
              return a + x + y ;   // 局所変数aをラムダ関数内で参照できる。
           }( 3 , 4 ) ) ;
   return 0 ;
}

Javaでラムダ式を使う例

Javaでも Java8 以降でラムダ式の機能が使えるようになっている。以下のように、ArrayList や Array の全要素に対して処理を行う forEach() メソッドでは、各要素に対して実行する関数を、無名関数として渡すことで、ループを回す繰り返し文を使わずにプログラムを記述できる。

// ArrayList<> の forEach() メソッド (Google Geminiの生成した例)
import java.util.List ;
import java.util.ArrayList ;

public class ForEachExample {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("Alice");
        names.add("Bob");
        names.add("Charlie");

        // forEachを使ってリストの要素を出力
        names.forEach( name -> System.out.println(name) );
    }               // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 無名関数(ラムダ式)
}                   //   型推論のおかげで name の変数宣言も書く必要がない...
                    //    names.forEach( (String name) -> System.out.println( name ) ) ;
-------------------------------------------------------------------------------------
// Array の forEach() メソッド
import java.util.Arrays ;

public class ForEachExample2 {
    public static void main(String[] args) {
        int[] numbers = {1, 2, 3, 4, 5};

        // 配列の要素を2倍にして出力
        Arrays.stream(numbers).forEach( number -> System.out.println(number * 2) );
    }                                // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 無名関数(ラムダ式)
}

Javaでは、ラムダ式は 関数型インタフェースと組み合わせて使う必要がある。forEach() メソッドは、Consumer という関数型インタフェースを持ち、引数に書いたラムダ式は、関数型インタフェースの実装として扱われている。

Javaで使えるラムダ式

Javaでは、Streamクラスでよく使われる関数インタフェースは以下の通り。これらの関数インタフェースを経由してラムダ式を使う必要があるが、クラスの型推論があるため、Predicate などの関数インタフェース名や呼び出しメソッドを自分で書くことはない。

  • Predicate<T> – 引数Tでboolean型を返す test(T) -> boolean
  • Supplier<R> – 引数なし で R型を返す get() -> R
  • Consumer<T> – 引数T で void型 accept(T) -> void
  • BiConsumer<T,U> – 引数T,U で void型 accept(T,U) -> void
  • Function<T,R> – 引数T で R型を返す apply(T) -> R
  • BiFunction<T,U,R> – 引数T,U で R型を返す apply(T,U) -> R
import java.util.*;

import java.util.function.Predicate ;
import java.util.function.Supplier ;
import java.util.function.Consumer ;
import java.util.function.BiConsumer ;
import java.util.function.Function ;
import java.util.function.BiFunction ;

public class Main {
    public static void main( String[] args ) {
        Predicate<Integer> even = (Integer x) -> { return x % 2 == 0 ; } ;
        System.out.println( even.test( 10 ) ) ;
        // ...filter( x -> x % 2 == 0 )...

        Supplier<String> greet = () -> "Hello" ;
        System.out.println( greet.get() ) ;
 
        // 1引数の Consumer<T> f = (T t) -> 式 ;
        //   Consumer は accept で呼び出す
        Consumer<Integer> foo = (Integer x) -> System.out.println( x ) ;
        foo.accept( 10 ) ;
 
        // ...forEach( x -> System.out.println( x ) ) ;
        // 2引数の BiConsumer<T,U> f = (T t , U u) -> 式
        BiConsumer<Integer,Integer> bar = (Integer x , Integer y) -> System.out.println( x * y ) ;
        bar.accept( 10 , 20 ) ;
        
        // 1引数で値を返す Function<T,R> f = (T t) -> { return 式 }
        //   Function は apply で呼び出す
        Function<Integer,Double> baz = (Integer x) -> Math.sqrt( x ) ;
        System.out.println( baz.apply( 5 ) ) ;
        // ...map( x -> x*x )...
 
        // 2引数で値を返す BiFunction
        BiFunction<Integer,Integer,Double> piyo = (Integer x , Integer y) -> Math.sqrt( x * y ) ;
        System.out.println( piyo.apply( 5 , 10 ) ) ;
    }
}