2009-12-02

シンタックスシュガーとしてのlambdaの解説

本の虫: lambda 完全解説で、lambdaの全機能は、ほぼ網羅したと思う。lambdaの文法は、それほど難しくはないと思うのだが、難しいと感じる人がいるらしい。とくに、キャプチャが理解できない人がいるそうだ。そこで今回は、lambdaの根本を解説してみようと思う。

lambdaというのは、そもそも関数オブジェクトのシンタックスシュガーなのである。例えば、

namespace hito {

template< class InputIterator, class Function >
Function for_each(
    InputIterator first,
    InputIterator last,
    Function f )
{
    for ( ; first != last ; ++first )
        f( *first ) ;

    return f ;
}

}


int main()
{
    std::vector<int> v ;

    for ( int i = 0 ; i != 10 ; ++i )
        v.push_back(i) ;

    hito::for_each(v.begin(), v.end(),
        [](int x){ std::cout << x << std::endl ; }
    ) ;
}

このコードは、これ以上ないくらいに分かりやすいC++のコードである。今、for_eachのしていることを分かりやすくするため、自前で実装してみた。for_eachは、イテレーターに対して、引数として與えられた関数オブジェクトを呼び出すのである。関数オブジェクトは、この場合、lambdaである。

とはいえ、lambdaを理解できない人間は、このコードもまた、理解できないのである。では、以下のように書き換えてはどうか。

struct lambda
{
    void operator ()( int value ) const
    {
        std::cout << value << std::endl ;
    }
} ;


int main()
{
    std::vector<int> v ;

    for ( int i = 0 ; i != 10 ; ++i )
        v.push_back(i) ;

    std::for_each(v.begin(), v.end(),
        lambda()
    ) ;
}

このように関数オブジェクトを渡しているに過ぎないのである。lambdaは所詮、関数オブジェクトのシンタックスシュガーであることが、実感できたことと思う。

しかし、変数のキャプチャはどうか。例えば以下のコード。

int main()
{
    std::vector<int> v ;

    for ( int i = 0 ; i != 10 ; ++i )
        v.push_back(i) ;

    int sum = 0 ;

    std::for_each(v.begin(), v.end(),
        [&]( int value ){ sum += value ; }
    ) ;

    std::cout << sum << std::endl ;
}

これは実に不可思議である。lambdaが関数オブジェクトであるのはまだしも理解もできようが、なぜ、その関数オブジェクトであるlambdaが、定義された場所の関数の変数を、自分でも使えるのか、理解に苦しむ。こんな奇妙なことが、単なる既存の関数オブジェクトのシンタックスシュガーであるわけがない――と思うかも知れない。

しかし、依然としてlambdaは、関数オブジェクトのシンタックスシュガーなのである。

class lambda
{
private:
    int & sum ;

public :
    lambda(int & ref) : sum( ref )
    { }
    
    void operator ()( int value ) const
    {
        sum += value ;
    }
} ;


int main()
{
    std::vector v ;

    for ( int i = 0 ; i != 10 ; ++i )
        v.push_back(i) ;

    int sum = 0 ;

    std::for_each(v.begin(), v.end(),
        lambda( sum )
    ) ;

    std::cout << sum << std::endl ;
}

ご覧のように、lambda関数オブジェクトは、コンストラクタの引数で、その関数内の変数を参照に取るだけである。lambdaのキャプチャとは、根本的には、この程度のものなのである。

C++0x以前では、関数オブジェクトは、お世辞にも、活用されているとは言い難かった。既存の関数ポインタより圧倒的に便利なのだが、如何せん、その文法が汚い。上記のlambda関数はいずれも、たった一行の、短いコードである。本質的には一行のコードを書くために、関数オブジェクトは、あまりにも文法上必要なゴミが多い。そのため、使用を敬遠されていたのだ。lambdaなら、その場所に、直に書くことが出来る。

lisp、python、javascript、ruby、C#等の言語を話す人からみたら、C++のlambdaは、制限が多いように思われるかも知れない。しかし、強い静的な言語であるC++としては、仕方がないのである。

1 comment:

萌ゑ said...

ラムダ式はhitoさんの説明で十分に理解出来ましたよ
これはいろいろ応用が利きそうですね