レベル: 初級 川合史朗, Writer, ITmedia
2007年 5月 25日 SchemeやLispによって30~50年前に導入されたさまざまな概念は、その後のプログラミング言語に多かれ少なかれ影響を与えました。そのうちの1つであるクロージャは、関数型言語では抽象化の基本的な方法となり、最近では多くのスクリプト言語にも採り入れられるようになってきています。本稿では、Scheme言語の処理系Gaucheを開発している川合史朗氏が、クロージャの機能を検証し、関数型言語とオブジェクト指向言語の関係について解説します。
"Object is a poor man's closure."Norman Adams"Closure is a poor man's object."Christian Queinnec
|
はじめに
2006年8月、Javaの次期仕様(JDK7)にクロージャを入れる案*が出され話題になりました。クロージャは関数型言語では30年以上の歴史を持ち、プログラミングにおける基本的な道具となっています。最近はPerl、Ruby、Pythonなどの軽量言語(Lightweight Language)にも取り込まれ、それと知らずに使っているユーザーも増えているのではないでしょうか。
一方、手続き型言語の世界では、完全なクロージャはなじみが薄いようです。C言語からC++へと発展してきた手続き型言語のメインストリームでは、
- ガーベジコレクションを持たず、ローカル変数をスタックに置くことを基本とする言語のため、クロージャとの相性が悪かった
- オブジェクト指向を採り入れることでクロージャの代用とできた
といったことが理由でしょう。
実際、クロージャとオブジェクト指向プログラミング言語には、メカニズム的に共通する部分があり、一方を他方で実装するのも簡単な工夫だけでできます。そのため、プログラミング言語に純粋性を求める向きには、「どちらか一方だけを言語のプリミティブとしてサポートすべきだ」との主張さえあります。確かに、クロージャを中心とするかオブジェクトを中心とするかで、プログラムの組み立て方が異なってくると言えます。
本稿では、クロージャとオブジェクトの実装と意味における共通部分と相違部分を検討することで、両方のメカニズムをうまく融合させる方法を考えてみたいと思います。なお、プログラム例としてSchemeを用いています*が、原理は多くのプログラミング言語に共通です。
クロージャによる動的オブジェクト
まずは簡単に、クロージャとは何かを説明し、それからオブジェクト指向との違いについて見ていきます。
クロージャおさらい
クロージャとは、関数が作成されるときの環境(定義個所から見えるローカルな束縛*)を「閉じ込んで」いるものです。Cのように関数の内部で関数を定義できない言語では、「ローカルな環境の内部で関数を作成する」ということがそもそもあり得ないため、クロージャの出る幕がありませんでした。しかし、多くの言語ではローカルな環境の中で関数を定義できて、その関数の中から外側にあるローカル変数を参照できます。
具体例を見てみましょう。次のSchemeコードは、map-add-nという関数を定義しています。add-map-nの内部では、ローカルな関数add-nを定義し、関数mapに渡しています。
(define (map-add-n n lis)
(define (add-n x) ← add-nを定義
(+ x n)) ← add-nを定義
(map add-n lis)) ← add-nを関数mapに渡している
|
関数add-nは「引数xにnを足す」、関数mapは「手続きとリストを取り、リストのおのおのの値に手続きを適用した結果をリストにして返す」というものです。従ってmap-add-nは「リストlisの各要素にnを足したもののリストを返す」という動作をします。
gosh> (map-add-n 5 '(1 2 3))
↑数値の1、2、3からなるリストの各要素にそれぞれ5を足す
=> (6 7 8) ← 実行結果
|
Schemeを見慣れない読者のために、同様のコードをPythonで書いておきます。
def map_add_n(n, lis):
def add_n(x):
return x+n
return map(add_n, lis)
|
Schemeのコードと比べて、括弧の位置など体裁を除けば、ほとんど同じだと分かるでしょう。この後に出てくる多くのコード例は、マクロを除けばほぼ一対一でほかの言語に変換可能です。読者のなじみの言語に変換しつつ読んでいただければ幸いです。
クロージャ=関数+環境
さて、ローカルに定義された関数add-nは、その外側にあるローカル変数nを参照しています。add-nが実際に呼ばれるのはmapの中であり、map本体は渡される関数のローカル環境など知らないわけですから、これを実現するadd-nは、処理内容「(+ x n)」だけでなく、map-add-nが作られた時点での束縛(前述の例では「n = 5」)をも知っている必要があります。関数mapに渡されるadd-nの実体は、単なる関数ポインタではなく、「n = 5」という環境と処理内容とを合わせたオブジェクトです。これをクロージャと呼ぶのでした。
もちろん、次にmap-add-nを呼び出せば、そのときに作られるnの束縛を環境として閉じ込んだ新たなクロージャが作られてmapに渡されます。
現在のJavaに慣れた人でしたら、inner classを使えば同じことができることに気づくでしょう。inner classは、「閉じ込む変数をプログラマーが明示的に指定している」という点を除けば、できることはクロージャとたいして変わりません。
メッセージ
「環境」を「状態」と見なせば、クロージャは「状態を持つ関数」となります。リスト1に挙げるmake-boxは、引数valueを閉じ込むクロージャdispatchを返します。実行例1のようにvalueに具体的な値を与えれば、その値を閉じ込んだクロージャが得られます。
リスト1 引数valueを閉じ込むクロージャdispatchを返すmake-box
(define (make-box value)
(define (dispatch msg)
(case msg
((get)
(lambda () value))
((set)
(lambda (newval) (set! value newval)))))
dispatch)
|
実行例1 make-boxに値を渡し、クロージャを得る
gosh> (define a-box (make-box 10))
↑引数を「10」としてmsg-boxを実行し、結果をa-boxに束縛する
a-box
gosh> a-box
#<closure (make-box dispatch)>
↑a-boxには、クロージャが束縛された
|
このa-boxはクロージャdispatchです。dispatch自体は引数msgを取り、その値がgetであれば、
すなわち「閉じ込まれたvalueを返すクロージャ」を、その値がsetであれば、
(lambda (newval) (set! value newval))
|
すなわち「引数newvalを取り、閉じ込まれたvalueの値をそれに変更するクロージャ」を、それぞれ返します。試しに実行してみましょう。
gosh> (a-box 'get) ←引数msgとしてgetを渡した
#<closure (make-box dispatch dispatch)>
↑クロージャが得られた
gosh> (a-box #39;set) ←引数msgとしてsetを渡した
#<closure (make-box dispatch dispatch)>
↑クロージャが得られた
|
「(a-box 'get)」、「(a-box 'set)」の戻り値はクロージャですから、それを呼び出せばアクションを起こすことができます。括弧がネストして見にくいので、これらを[a-box'get]、[a-box'set]のように表記してみましょう。多くのScheme処理系では、大括弧[]を()と同様に使うことができます。
gosh> (define a-box (make-box 10))
a-box
gosh> ([a-box'get])
10
gosh> ([a-box'set] 11)
11
gosh> ([a-box'get])
11
|
この手法は、dispatchが判別するメッセージの種類とその内容さえ変えれば、いろいろと応用が利きます。dispatchの本体は定型なので、これをマクロで書いてみましょう。
リスト2 dispatchの本体を書きやすくするためのマクロ。3、4行目のようなパターン(式)があるなら、5行目以降のように式を変換するというもの
(define-syntax object-maker
(syntax-rules ()
[(object-maker (ivar ...)
(method args . body) ...)
(lambda (ivar ...)
(define (dispatch msg)
(case msg
((method) (lambda args . body)) ...))
dispatch)]
))
|
リスト2の定義は、
(object-maker (ivar ...) (method args . body) ...)
|
という式が出てきたらそれを、
という式に変換せよ、ということです。一部括弧に[]を使っているのは見やすさのためで、()を使うのと意味的な違いはありません。また、プログラム中の「...」は説明のために省略したわけではなく、れっきとしたSchemeの構文要素です。「(x ...)」で、「xが0個以上繰り返される」ということを示しています。
このマクロを使えば、リスト1のmake-boxは次のように書くことができます。
(define make-box
(object-maker (value)
[get () value]
[set (newval) (set! value newval) value]))
|
構文[a-box'get]や[a-box'set]を、a-box.getやa-box.setに読みかえると分かりやすくなりますが、make-boxが返す「もの」は、
- 状態を持ち
- メッセージを受け取って状態を変えたりアクションを起こす
という意味では、オブジェクト指向における「オブジェクト」の基本的な性質*を持っていると言えます。このメカニズムを基本に、継承やクラスといった、主流のオブジェクト指向言語に備わっている機能を足してゆくことは難しくありません*。
クロージャ≡オブジェクト(mod構文)
前節を読んで、「単にオブジェクトっぽいものを、クロージャを使って作ってみせただけではないか」と思われたかもしれません。では、make-boxが返す「何か」と、あなたが考える「オブジェクト」との本質的な違いは何でしょうか。
「言語としてネイティブでサポートされているか否か」というのは、言語を使う上での利便性には大きく影響します。しかしそれは、書かれたプログラムの意味に本質的な影響を与えるものではありません。
前節のmake-boxの定義は、オブジェクトの1つの実装であると同時に、「メッセージベースのオブジェクトシステム」の1つの仕様定義であると考えることもできます。make-boxが返す「何か」とは、前節で与えられた仕様を満たす「何か」です(それが実際に前節のコードで実装されているかどうかにかかわらず)。その「何か」は固有の状態を持ち、メッセージを受け取ってアクションを起こします。ある人はそれをクロージャと呼び、ある人はそれをオブジェクトと呼ぶでしょう。
幾つかあるオブジェクト指向の定義のうち、少なくとも「状態と処理が合わさっていて、メッセージを送ることでアクションを起こさせるもの」という定義に関して、クロージャとオブジェクトの違いはせいぜい構文の問題にすぎません(もちろん構文は言語の使い勝手に大きな影響を与えますが、マクロの例を示したように、表面的に処理できるものでもあります)。
……でも、そう言いきられても素直に納得できない読者が多いのではないでしょうか。何か、反論したいようなもやもやを感じるのではないですか? そのもやもやとは何なのでしょう。次回はこの点について解説していきます。
このページで出てきた専門用語
Javaの次期仕様(JDK7)にクロージャを入れる案
提案の共著者であるNeal Gafterのブログ「Closures for Java」が分かりやすいでしょう。
プログラム例としてSchemeを用いています
ここで紹介したSchemeのコードは、すべてScheme処理系Gaucheでテストしています。動かして試しながら理解したいという方は、Gaucheをインストールしてください。なお、2006年8月時点で、GaucheはLinux/Mac OS X/*BSD/Windowsで動作します(ただし、Windows版はCygwin環境で動作するものです。ネイティブ環境で動作する版もありますが、まだ動作は不安定です)。
束縛
手続き型言語で言うところの、変数の定義や代入に相当します。「ローカルな束縛」とは、入れ子になった関数の中で定義された変数を指します。ローカルな変数は、入れ子の内部の関数からのみ参照/更新できます。
関数
ここでは「関数」を、C言語などの、処理内容のみを持つ命令列、というような意味で使っています。関数型言語の多くはむしろすべての関数が環境を持つのが当然であり(グローバルな関数はたまたま環境が空であると考える)、そこでは「関数」と「クロージャ」はほぼ同義で使われます。
オブジェクト指向における「オブジェクト」の基本的な性質
何をもってオブジェクト指向とするかについては、唯一の定義といったものは存在しません。Jonathan Reesのメモでは、幾つかの機能や特性を挙げ、人によって異なるサブセットをオブジェクト指向の定義としている、と述べています。
主流のオブジェクト指向言語に備わっている機能を足してゆくことは難しくありません
継承は、dispatchが認識できないメッセージに出合ったら「親オブジェクト」に処理を移譲すれば良いでしょう。またクラスは、「ある特定のプロトコルを満たすオブジェクト」として実装できます。なお、クラスや継承、メソッド定義構文を持った、より完全なオブジェクトシステムをクロージャ上で構築する方法については、例えばKen Dickeyの「Scheming with objects」*を参照すると良いでしょう。
「Scheming with objects」
Ken Dickey: "Schmeing with Objects", Computer Language, October 1992.
ftp://ftp.cs.indiana.edu/pub/scheme-repository/doc/pubs/swob.txt
参考文献
著者について  | |  | ITmedia |
記事の評価
|