大人のためのブラックボックス読解講座――クロージャとオブジェクトの微妙な関係プログラミング言語の進化を追え(1/2 ページ)

SchemeやLispによって、30〜50年前に導入されたさまざまな概念は、その後のプログラミング言語に多かれ少なかれ影響を与えました。そのうちの1つであるクロージャは、関数型言語では抽象化の基本的な方法となり、最近では多くのスクリプト言語にも採り入れられるようになってきています。本稿では、Scheme言語の処理系、Gaucheを開発している川合史朗氏が、クロージャの機能を検証し、関数型言語とオブジェクト指向言語の関係について解説します。

» 2007年03月29日 15時30分 公開
[川合史朗,ITmedia]

"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は、「閉じ込む変数をプログラマーが明示的に指定している」という点を除けば、できることはクロージャとたいして変わりません。

このページで出てきた専門用語

Javaの次期仕様(JDK7)にクロージャを入れる案

提案の共著者であるNeal Gafterのブログ「Closures for Java」が分かりやすい。

プログラム例としてSchemeを用いています

ここで紹介したSchemeのコードは、すべてScheme処理系Gaucheでテストしている。動かして試しながら理解したいという方は、Gaucheをインストールされたい。なお、2006年8月時点で、GaucheはLinux/Mac OS X/*BSD/Windowsで動作する(ただし、Windows版はCygwin環境で動作する。ネイティブ環境で動作する版もあるが、まだ動作は不安定)。

束縛

手続き型言語で言うところの、変数の定義や代入に相当する。「ローカルな束縛」とは、入れ子になった関数の中で定義された変数を指す。ローカルな変数は、入れ子の内部の関数からのみ参照/更新できる。

関数

ここでは「関数」をC言語などの、処理内容のみを持つ命令列、というような意味で使っている。関数型言語の多くはむしろすべての関数が環境を持つのが当然であり(グローバルな関数はたまたま環境が空であると考える)、そこでは「関数」と「クロージャ」はほぼ同義で使われる。


       1|2 次のページへ

Copyright © ITmedia, Inc. All Rights Reserved.

注目のテーマ