クロージャ・無名関数・関数オブジェクトに関する世界観です。関数型プログラミングも参照のこと。
クロージャは、JavaScriptやRubyでおなじみの、関数を定義したスコープにある変数を使用できる関数のこと。
関数の定義されたのと同一スコープに定義されたローカル変数に、関数の内部からアクセスするような関数のことを、クロージャと呼ぶ。
Lispのような関数型言語の考え方である。上手く使うことで、簡易オブジェクト指向(さまざまな関数から共通のデータを参照する)のような使い方もできる。
一部の言語では、クロージャを無名関数やラムダ式で行っている。無名関数は、昔デリゲートなどと呼ばれてC#などで提供されていたこともあり、C#ではWindows.Formsのイベントにメソッドを登録する際に無名関数は便利である。
たとえば、
function f() { var value = 1; function g() { console.log(value); } g(); value = 3; g(); } f();
実行結果:
1 3
このような時、g()は同じスコープにある変数valueを参照するクロージャであり、valueの値は後々になって変えることもできる。
また、
var f = function() { var value = 0; return function g(number) { value += number; console.log(value); }; }; add = f(); add(3); add(6);
実行結果:
3 9
このように、関数から関数を返し、この関数をクロージャとして使うこともできる。変数valueの値はクロージャとともに保持される。
状態も参照のこと。
関数型プログラミングでは、デリゲートや関数オブジェクト、lambda式などの「関数から関数を呼び出すシステム」が存在する。
デリゲートとは「委譲する」という意味で、関数を別の場所から間接的に呼び出すことのできるシステム。
関数オブジェクト(functor)は、関数に対して関数を与えるデリゲートと同様の仕組み。なんのために使うかといえば、やねうらお氏の「Windowsプロフェッショナルゲームプログラミング2」で述べられているように、C++のrandam_shuffleは乱数生成アルゴリズムとしてデフォルトではrand()が呼び出されるが、この部分だけをカスタマイズする、すなわちrand()ではなく別の関数を呼び出させるように関数に別の「関数オブジェクトを与える」という使い方をする。
僕個人のアイデアとして、たとえば「関数によって返される関数オブジェクトを関数の引数に与える」という、まるでLispハッカーのような考え方もできる。xが1でyが2であるときは1+2を返し、2と3であるときは2+3を返すような「x+yの足し算」を行う関数や、それを単純に足し算から引き算、掛け算、割り算、あるいは累乗や平方根の計算結果を返すような、まるで「関数なんでも生成器」のような関数オブジェクトとして実行可能な関数を戻り値として返す関数を作り、その関数に名前をつけることなく、即座に別の関数に「この関数生成器で作られた関数を使って計算してね」という風にプログラミングを行うことができる。たとえば「スタックにある全てのデータに対して再帰的に処理をする関数」を作り、この関数に関数オブジェクトを「具体的に処理する内容」として引数に与えることもできる。関数オブジェクトは名前のついた関数だけではなく、関数オブジェクトを返す関数によって返された関数オブジェクトでも構わない。
関数オブジェクトは、functorと呼ばれるほかにlambdaとも呼ばれる。functorという言葉は、operator ()をオーバーロードしたクラスを指す際にも使われる言葉であり、Lispなど関数型言語ではlambdaという言葉の方が馴染み深いかもしれない。単なる無名関数だとか、関数表現の縮小形と思っている人も多いかもしれないが、Lispではめちゃくちゃ使える機能であるとして、Lispハッカーのポール・グレアムも「C++を知るとBASICが使いづらく感じるようになるように、Lispを知るとC++が使いづらく感じるようになる」と言っている。
詳しくは以下の書籍が参考になります。
実際のところ、プログラミングにおいて、関数すなわちコードブロックをデータとして扱えると、いいことがたくさんあります。
C言語では、関数ポインタなどの例外的方法を使わない限り、関数をデータとして扱うことができません。
ですが、Lispなどの関数型言語では、関数をデータとして扱うことができます。
関数をデータとして扱えると、たとえば「関数に関数を指定して引数として渡す」ということができます。このような処理を関数に対してお願いする、すなわち「委譲」することができます。
この結果、コールバック関数やイベントなどに基づく非同期処理、map()のような高階関数を用いることができます。
このような方法は、GUIのようなイベント処理を用いる技術や、OpenGLのようなグラフィックス技術、あるいは並列処理、非同期処理を用いるネットワークサーバーなどでも、よく使われる方法です。
そもそも、C言語のプログラムは、コマンドで使うことを基本的なスタイルとしていますが、コマンドを常に同期実行するということは、プログラミング全体から言えば多くはありません。
C言語ではなないC++では、テンプレートを使った汎用関数や汎用クラスだけではなく、STLのコンテナやイテレータやアルゴリズム、そして関数オブジェクトの機能を搭載しています。
C++/STLの機能の目的は、こうした「関数をデータとして扱う」ということを、「型を汎用的に扱う」ということと一緒にして、「どんな型やどんなコードブロックであっても後々から指定できるようにしよう」という意図があると思います。
関数をデータとして扱うことで、ポインタで同じデータにアクセスするだけではなく、関数オブジェクトによって同じコードブロックにアクセスできます。ポインタでデータを後々になって渡し、同じデータにアクセス・操作するだけではなく、関数オブジェクトによって、同じコードブロックにアクセス・操作できます。後々になって定義したデータやコードブロックを渡すこともできます。
これはGUIのイベント駆動プログラミングでは、「ボタンやメニューがクリックされたらこのコールバック関数を実行してほしい」と後々からお願いをするという意味で、非常に有用です。ネットワークサーバー、たとえばNode.jsなどでも、「このようなリクエストが来たらこのコールバック関数を実行してほしい」という風に、同様に有用です。
僕の自論として、関数にコードブロックを与えることができないのはおかしいと僕は信じています。
関数には、自らの記述したコードブロックを、それを適切な場所で実行するための無名関数として、与えられるべきであると僕は信じています。
たとえば、Rubyなどではブロックを用いることで、関数にコードブロックを与えることができますが、それこそが正しい言語です。
昔C#で使われていたデリゲートなども同様で、僕はかつてよりそのような関数にコードブロックを与えることのできるRubyやC#の大ファンでした。
逆に、そのような使い方が制限される、C言語の関数ポインタのようなものを僕は好みません。Lispほどの抽象化ではなくとも、関数にコードブロックを与えることは言語にとって必須要素だと感じています。
真に汎用的な言語を作るためには、関数にコードブロックを与えることが許されていなければならないのです。
たとえば、GUIプログラミングにおいて、メソッドはイベントに紐づけることができますが、そのような場合にも、無名関数あるいはブロックを用いてイベントにメソッドを紐づけられるべきなのです。
「関数として名前を付ければいいじゃないか」と思われる人もいるでしょうが、「コードブロックには名前を必ず付けなければならない」というルールはおかしいのです。コードブロックに名前がないということは僕にとって直感的な感覚要素であり、コードブロックにすべて名前を付けなければならないような言語は僕は嫌いです。名前を付けるべきなのはコードブロックを与える関数のほうであり、コードブロックのほうにも名前を付けなければならないのは僕は違和感があります。
ただし、自らの独自の関数を、名前を付けて宣言すること自体は僕は嫌いではありません。なぜなら、「自分の名前で自分の関数を作る」ということができるということは、「言語の拡張」に繋がります。すなわち、言語をそのまま使うのではなく、自らの作った独自の関数を使って、言語そのものを拡張できます。あるいは、そこまで言わなくても、「自分の独自の便利に呼び出せる関数を作る」ということはプログラミングの基本であり、それこそがまさに「プログラミングの愉しい点」であると言えるでしょう。
そして、そのような関数に対して、いつでも任意のコードブロックを与えられるようにすることが、真に「汎用的な文法要素を作る」ということ、すなわち、「言語を拡張する」ということに繋がるのです。
たとえば、if文やfor文を使う時に、わざわざ内部のブロックに名前は付けませんよね。if文やfor文のキーワードとして「if」とか「for」というキーワードや識別子は使いますが、その内部のブロックには名前は付けません。プログラミングとはそういうものであり、コードブロックには名前を付けず、コードブロックを与える関数のほうを名前を指定して呼び出すべきなのです。
2024.06.18