seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

ラムダ/クロージャ/ブロックの覚え書き

C#, C++, Objective-Cのラムダ(匿名関数)の基本的な使い方の覚え書きと、JavaSE7時点のラムダ相当のイディオムについての雑記。

(※ プログラムの作用に主眼を置いて、単純に、Lambda ≒ Closure ≒ Block ≒ 匿名関数 という認識で書いてます。)

C#4.0のラムダの使い方

C#には、C#1.0のもとより関数ポインタ的なdelegateという言語的な仕組みがあって、
それに乗っかる形でC#4.0よりラムダがサポートされた。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace ClosureCs
{
    class Program
    {
        static void Main(string[] args)
        {
            // C#4.0はFunc, Actionなどのデリゲートが予め沢山用意されているので
            // 自分でdelegateを宣言する必要はほとんどない。
            var funcs = new List<Func<int, int>>();

            for (int idx = 0; idx < 10; idx++)
            {
                // idxは逐次変更されてゆく変数なので、ラムダの中から参照すると、
                // その時点でのidxの値、今回の場合はforループの最終値を見てしまう.
                // 現在の値をキャプチャするために、一旦、スコープ内で変数宣言する.
                int i = idx;

                // ラムダは関数なので、他の関数同様に同じ引数と型をもつdelegateに代入可能.
                Func<int, int> func = (offset) => offset + i * 2;
                funcs.Add(func); 
            }

            var buf = new StringBuilder();

            int count = 0;

            CallFuncs(100, funcs, (ret) => // ラムダ文
            {
                // レキシカルスコープ上の変数に普通にアクセス可能
                // 読み書き自由.
                count++;
                buf.Append(ret + "\r\n");
            });

            Console.WriteLine(string.Format("count={0}\r\n{1}", count, buf));
        }

        static void CallFuncs(
            int offset,
            IEnumerable<Func<int, int>> funcs,
            Action<int> callback
            )
        {
            foreach (var func in funcs)
            {
                callback(func(offset));
            }
        }
    }
}

ラムダ式、ラムダ文を変数として格納するには、他のメソッドの場合と同様にdelegate に入れればよいが、
C#4.0には事前定義済みのFunc<>とかAction<>が沢山あるので
自分でdelegateを定義する必要性はほとんどない。


なお、ラムダ式側の仮引数には型の指定方法が存在せず、戻り値の型の指定方法もないので、それ単体で型を確定できない。
そのため、var変数への代入はできない。


C#ではラムダの中から外の変数は普通に参照でき、且つ、読み書きできる。
ラムダの中から見える変数はラムダ作成時のキャプチャ(スナップショット)ではなく、その変数への生きた参照である。


ラムダ作成時点の変数のキャプチャが欲しい場合は、ラムダ作成時のみ有効なスコープ上に変数を割り当てることで明示的に行うことができる。

C++11のラムダの使い方

C++0xもといC++11より、C++でもラムダがサポートされるようになった。

#include "stdafx.h"

#include <algorithm>
#include <functional>
#include <iostream>
#include <list>
#include <memory>
#include <sstream>

typedef std::list<std::function<int(int)>> closure_list;

void callFuncs(
	int offset,
	const closure_list &funcs,
	std::function<void(int)> callback
	)
{
	std::for_each(
		funcs.begin(),
		funcs.end(),
		[=, &callback](std::function<int(int)> fp) {
			// キャプチャの[=]指定で、
			// レキシカルスコープ上の全ての変更をコピーによるアクセス可能とする.
			// ただし、callbackは参照とする.
			auto f = fp;
			callback(f(offset));
	});
}

int _tmain(int argc, _TCHAR* argv[])
{
	closure_list funcs;

	for (int idx = 0; idx < 10; idx++) {
		// キャプチャで明示的にidxを取込み(コピー)する.
		// idxの、この時点でのスナップショットがラムダ式の中で使われる.
		auto func = [idx](int offset){ return offset + idx * 2; };

		// ラムダはstd::functionでオブジェクト化できる
		std::function<int(int)> fp = func;
		funcs.push_back(fp);
	}

	std::ostringstream os;

	int count = 0;

	callFuncs(100, funcs, [&](int ret) -> void {
		// キャプチャの[&]指定で、
		// レキシカルスコープ上の全て変数を参照可能にする.
		// (戻り値の型は省略可能であるが、「->」で明示的に指定することもできる。)
		count++;
		os << ret << std::endl;
	});

	std::cout << "count=" << count << std::endl << os.str();

	return 0;
}

C++はオブジェクトの寿命管理をプログラム的に行う必要がある言語である。


ラムダではラムダを作成する時点と使用する時点とがタイミング的に異なりえるため、
ラムダの中から外側の変数へのアクセスを保証する仕組みが必要となる。

それが「キャプチャ」という仕組みである。


ラムダ式[キャプチャ](仮引数){ステートメント;} の文法で示されるもので、
ラムダの中で参照する外側の変数はキャプチャによって示されなければならない。


キャプチャの方法としては二つあり、一つはコピー、もうひとつは参照である。

  • [] の中に変数名を指定することで、ラムダ作成時点の変数の値がコピーされ、ラムダを実行するときにはコピーされた値が使われる。(コピーされた変数に対する書き込みは禁止される。)
  • [] の中に&つきの変数名を指定することで、変数へのアクセスは参照となる。ラムダの外の変数への読み書きが可能であるが、ラムダが実行される時点で参照先が存命であることはプログラマが保証しなければならない。

ラムダの中でアクセスする変数をいちいち列挙するのも大変なので、省略記法として [=], [&]を使うことができる。
これはラムダの中で使う外部の変数は、全部コピー([=])、もしくは全部参照([&])、という指定となる。

一部だけ扱いを変える場合は、[=, &ref] のように後付けで個別の変数で指定することも可能である。


また、キャプチャによってコピーされた場合、ラムダの中で、その変数を書き換えることはできず、
ラムダ構築時の値の定数のように扱う必要がある。(代入しようとするとコンパイルエラーとなる。)


ただし、以下のように mutable を指定することで書き換え可能にできる。

	int x = 0;
	auto f = [=]() mutable -> void {
		x++;
		std::cout << x << std::endl;
	};
	f();
	f();
	std::cout << x << std::endl;

結果は、

1
2
0

となる。

上の二行はf()の呼び出しで、ラムダを構築したときにコピーしたxの値をmutable指定によりラムダ内部で書き換えていることを示す。
しかし、コピーであるため最後の行のように、元々の変数には変化はない。
(見ての通り、副作用が大きく、うまく使うのは難しいような感じである。)


C#の場合と異なり、C++のラムダは仮引数に型を指定する必要があり、戻り値はreturnがあれば、それより推論し、また明示的に戻り値型を指定することもできる。
そのため、ラムダはauto変数に格納することが可能である。


ラムダをauto変数ではない型指定された変数に格納したり、関数の仮引数にしたり、戻り値の型とする場合には、std::function を使うことができる。

Objective-Cのブロックの使い方

Objective-Cにも、ラムダ(クロージャ)相当の「ブロック」という仕組みがある。

#import <Foundation/Foundation.h>

// ブロック型の定義 (関数ポインタに相当)
typedef int (^func_t)(int);
typedef void (^action_t)(int);

void callFuncs(int offset, NSArray *arr, action_t action)
{
    // 高速列挙(NSFastEnumerationを実装しているものはforeachできる)
    for (func_t func in arr) {
        action(func(100));
    }
}

int main(int argc, const char * argv[])
{
    @autoreleasepool {
        
        id arr = [NSMutableArray array];
        
        for (int idx = 0; idx < 10; idx++) {
            func_t func = ^(int offset){
                // ブロックが作られた時点の自動変数がキャプチャされる.
                // このブロックを実行した場合、作られた時点の変数が見れるということ.
                return offset + idx * 2;
            };
            [arr addObject: func];
        }

        // ブロックの中から書き換える場合は __block属性をつける
        int __block count = 0;
        
        id buf = [NSMutableString string];
        callFuncs(100, arr, ^(int ret) {
            count++;
            // 自動変数がキャプチャされているので
            // bufに対する参照(ポインタのコピー)は、そのまま使える.
            [buf appendString: [NSString stringWithFormat: @"%d\n", ret]];
        });

        printf("count=%d\n%s", count, [buf UTF8String]);
    }
    return 0;
}

(※ もう少し詳しく実証したコードは下に追記しています。)


Objective-Cは、程度の差はあれ、基本的にはC++同様にプログラマがメモリ管理する言語である。
これはC言語をベースとしている以上は仕方ないことかもしれない。
(Lion以降ではスコープを抜けたときに参照カウンタを自動的に減じるなどのコンパイラサポートが非常に手厚くはなっている。が、JavaC#のようにメモリ管理が完全にランタイムの仕事というわけではなく不注意によってダングリングポインタなどを作ってしまう可能性はある。)


そのためであろうか、ブロックの考え方はC++のラムダとよく似ているように思われる。
(Blocksは、Snow Leopardからなので、時間的にいえばObjective-CのブロックのほうがC++11のLambdaより先にリリースされたけれど。)


ただ、キャプチャを明示的に記述する必要はなくて、ブロック内で使われている外部の変数は暗黙にキャプチャされるということ。

このキャプチャは「コピー」であり、ブロック内で外部の変数には読み取りアクセスできるが変数の書き換えはできない。
(書き換えようとするとコンパイルエラーになる。)


もし、書き換えがしたい場合は、その変数に対してあらかじめ「__block」という属性をつけておく必要がある。
このようにして宣言された変数はブロック内から書き換えられるようになる。

JavaSE7までの匿名クラスの使い方 (おまけ)

JavaSE8よりJavaにもラムダ構文が導入されることになるようであるが、現時点のJavaSE7には純粋なラムダは存在しない。


が、実際のところ、ラムダを単なる匿名関数、クロージャと見立てれば、これに良く似たものはJava1.1の太古からある。
それが匿名クラス(無名インナークラス)である。


Javaをまともに使った事があれば匿名クラスは知っているだろうし、SwingなどのデスクトップアプリでJavaを使えば匿名クラスは無くてはならないぐらい多用されるものだと思うのだが、Javaを良く知らない人には、その存在を知らない人も多いようである。

package anonymous_block;

import java.util.ArrayList;
import java.util.List;

public class Main {

    /**
     * 数値を引数にとり、数値を返す匿名クラスのためのインターフェイス
     *
     * @author seraphy
     */
    interface Func {

        /**
         * 数値を受け取り数値を返す関数
         *
         * @param offset 引数
         * @return 結果
         */
        int func(int offset);
    }

    /**
     * 数値を引数にとり何も返さない匿名クラスのためのインターフェイス
     *
     * @author seraphy
     */
    interface Action {

        /**
         * 数値を受け取るアクション
         *
         * @param ret 受け取る数値
         */
        void action(int ret);
    }

    /**
     * メインエントリ
     *
     * @param args コマンドライン引数
     * @throws Exception 失敗
     */
    public static void main(String[] args) throws Exception {

        List<Func> funcs = new ArrayList<>();

        for (int idx = 0; idx < 10; idx++) {
            // 匿名クラス内で使えるようにfinalで拘束する.
            // final宣言されていれば匿名クラスの外の変数にもアクセスできる.
            final int i = idx;

            // 匿名クラスはクロージャのような性質をもつ。
            // ただし型の事前定義が必要で、かならずクラスが生成される。
            funcs.add(new Func() {

                @Override
                public int func(int offset) {
                    return offset + i * 2;
                }
            });
        }

        // JAVA言語では、final宣言しても変数にオブジェクトが拘束されるだけ、
        // つまり変数を別のオブジェクトやnullに設定できなくなるだけである。
        // (オブジェクトそのものがimmutableになるわけではない。)
        final StringBuilder buf = new StringBuilder();

        // finalで参照しないかぎり匿名クラスの中からアクセスできないが
        // finalだと変数の書き換えができない.
        // そのため、回避策として、配列オブジェクトとして変数に拘束させて、
        // 配列へのアクセスを通じて値の書き換えを行う.
        final int[] count = new int[]{0};
        
        callFuncs(100, funcs, new Action() {

            @Override
            public void action(int ret) {
                // finalで拘束されている変数は参照可能である
                count[0]++;
                buf.append(ret).append("\r\n");
            }
        });

        System.out.println(String.format("count=%s\r\n%s", count[0], buf));
    }

    /**
     * 匿名クラスのリストを受け取り、それを全て実行しつつ、 その結果をコールバックするメソッド。 とはいえ、型は宣言する必要があるので、
     * 引数の見た目も普通のメソッドになる。
     *
     * @param offset オフセット
     * @param funcs 匿名クラスのリスト
     * @param action コールバック先
     */
    private static void callFuncs(int offset, List<Func> funcs, Action action) {
        for (Func func : funcs) {
            action.action(func.func(offset));
        }
    }
}

JavaSE7までの、この匿名クラスの特徴としては、

  • いちいち「クラスまたはインターフェイスの実装」という形式になるので、書くコード量が無駄に多くなる。
  • C#の事前定義されたデリゲートのように、事前定義されたインターフェイスがあればいいのに、そうゆうのがないので自分で作る必要がある。
  • finalで拘束された変数しか匿名クラスの中からは参照できないので、扱いが煩雑になりがちである。
  • 結局、1つ1つ、明示的な型を持ったオブジェクトでなければならず、式を受けて式を加工して式として返すようなことも難しい。
  • 汎用的に使えないためコレクションクラスなどの標準ライブラリとの連携もできず、使い道が自作クラス周辺に限定されてしまう。

という点が挙げられるだろうか。

後半のいくつかは、真のラムダでないことに起因する問題と言えよう。


とりあえず、ラムダっぽいことをやるだけなら、現時点のJavaSE7でも十分可能である、といえる。
が、コレクションクラスと連携強化されLinq的な使い方が可能になるJavaSE8が待ち遠しいのも確かではある。

結論

Objective-C, C#, C++のラムダやブロックを使うために調べたことを、最低限のひな形として記述できたと思う。

しかし、以下の点が心残りである.

  • ラムダ = クロージャ = 匿名関数、という認識で書いているのだが…。
  • C#C++で何故Lambdaと呼ばれているのか、よくわからない…。ClosureとかBlockでいいんじゃないの?
  • というか、Lambdaって何なの?


※ 2015/5/28追記

  • ラムダとは匿名関数/無名関数の表記方法のことのみを指す。*1
    • それがクロージャの性質をもっているかどうかは問わない。
  • クロージャとはレキシカルコンテキストを持ちまわることのできる関数オブジェクトである*2
    • Java8でサポートされたラムダや、従来の匿名クラスでは、外側のローカル変数の読み込みは可能だが、暗黙でfinal扱いになり「書き換え」する方法がないため完全なクロージャとはいえない。*3 *4
    • C#は外側の変数を読み書きできる。C++/Objective-Cも外側の変数に対する修飾子などを指定すれば書き込み可能となるため、これらはクロージャをサポートしているといえる。

...ってことみたい。


C言語系4種そろってラムダをサポートするご時世だし、そろそろ関数型言語を勉強しなきゃかな...。

以上、メモ終了

*1:http://www.yefisys.com/blog/2013/12/29/overview-of-closure-and-lambda-expression/

*2:http://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AD%E3%83%BC%E3%82%B8%E3%83%A3

*3:Java8のlambda構文がどのようにクロージャーではないか

*4:ただし、オブジェクトへの参照はできるため1要素の配列オブジェクトを値の入れ物として見立ててラムダ内や無名クラス内から外側に値を設定することはできる。また、ラムダを囲んでいるクラスのメンバ変数へは読み書きアクセスできる。