Java.next: 継承を伴わない拡張、第 2 回

Clojure のプロトコルについて探る

Java 言語の拡張メカニズムには意図的な制約があり、Java 言語を拡張する際には主に継承とインターフェースを利用して行わざるを得ません。その一方で、Groovy、Scala、Clojure には、これに代わるもっと多くの拡張手段があります。今回の記事では、Clojure が拡張メカニズムとしてプロトコルを使用する方法を詳しく探ります。

Neal Ford, Director / Software Architect / Meme Wrangler, ThoughtWorks Inc.

Neal Fordの画像Neal Ford は世界的な IT コンサルティング企業 ThoughtWorks のディレクターであり、ソフトウェア・アーキテクトであり、Meme Wrangler でもあります。また彼は、アプリケーション、教育資料、雑誌記事、コースウェア、ビデオや DVD によるプレゼンテーションなどの設計と開発も行っています。さまざまな技術に関する本の著者、編集者でもあり、最新の著作は『Presentation Patterns』です。彼は大規模なエンタープライズ・アプリケーションの設計や構築を専門にしています。また彼は世界各地で開催される開発者会議での講演者としても国際的に有名です。彼の Web サイトをご覧ください。



2013年 9月 05日

この連載について

Java の遺産となるのは、プラットフォームであって、言語ではないでしょう。200 を超える言語が JVM 上で実行されている今、最終的にこれらの言語の 1 つが JVM のプログラミングに最適な方法としてJava 言語に取って代わることは避けられません。この連載では、Java 開発者が自分たちの近い将来を垣間見ることができるように、3 つの次世代 JVM 言語 ― Groovy、Scala、Clojure ― について、新しい機能やパラダイムを比較対照することで、詳しく探ります。

Java.next: 継承を伴わない拡張、第 1 回」では、Java.next 言語が継承を伴わずに拡張を実現する数ある方法の 1 つとして、既存のクラスに新たなメソッドを追加する Groovy、Scala、Clojure のメカニズムに焦点を当てました。今回の記事では、Clojure のプロトコルがどのようにして、Java の拡張機能を斬新な方法で拡張し、Expression Problem に対して洗練されたソリューションを提供しているかを探ります。

今回の記事での主な関心は拡張性にありますが、Clojure と Java のコードをシームレスに相互運用できるようにする Clojure の機能についてもいくつか取り上げます。この 2 つの言語のコアとなる部分は明らかに異なりますが (Java は命令型でオブジェクト指向であり、Clojure は関数型です)、Clojure には Java の構成体を最小限の手間で扱えるようにする便利な手段がいくつか実装されています。

Clojure のプロトコルの復習

プロトコルは、Clojure エコシステムの重要な要素です。前回の記事では、既存のクラスにメソッドを追加する手段として、プロトコルを使用する方法を説明しました。プロトコルは、オブジェクト指向言語でおなじみの機能の多くを Clojure で模倣する際にも役立ちます。例えば、Clojure はプロトコルによって「レコード」と「関数」をバインドすることで、オブジェクト指向のクラス (データとメソッドの組み合わせ) を模倣します。プロトコルとレコードの間のやりとりを理解するには、まず始めに Clojure においてレコードの基礎をなすコア・データ構造である「マップ」について説明する必要があります。

マップとレコード

Clojure では、マップは名前と値のペアのコレクションです (他の言語ではおなじみの概念です)。例えば、リスト 1 に示す REPL (Read-Eval-Print Loop) での操作は、Clojure プログラミング言語についての情報を含むマップを作成することから始めています。

リスト 1. Clojure のマップを扱う
user=> (def language {:name "Clojure" :designer "Hickey" })
#'user/language
user=> (get language :name)
"Clojure"
user=> (:name language)
"Clojure"
user=> (:designer language)
"Hickey"

Clojure ではマップを多用することから、Clojure にはマップを簡単に扱えるようにするための特別な構文糖があります。キーに関連付けられた値を取得するには、おなじみの (get ) 関数を使用することができます。しかし Clojure ではこのような一般的な操作でも、より冗長でないものにしようとします。

Java の環境では、Java 言語のソース・コードはネイティブ・データ構造ではないため、構文解析して変換する必要があります。一方 Clojure (および、その他の Lisp のバリエーション) では、ソース・コードの表現はネイティブ・データ構造になっています (例えば、言語の奇妙な構文を説明するのに役立つ「リスト」など)。Lisp インタープリターがリストをソース・コードとして読み込むと、リストの先頭の要素を「呼び出し可能な」何か (関数など) に変換しようとします。したがってリスト 1 では、(:name language) 式は (get language :name) 式と同じ結果を返します。Clojure がこの構文糖を提供しているのは、マップから項目を取得するのは一般的な操作であるためです。

さらに Clojure では、いくつかの構成体を関数呼び出しのように扱うことができ、それによって呼び出し可能な機能 ― つまり関数のように呼び出せる機能 ― を拡張しています。Java プログラムで呼び出すことができるのは、メソッドと、言語に組み込まれているステートメントのみです。リスト 1 には、Clojure では (:name language) のように、マップのキーが関数呼び出しとして機能できることが示されています。マップ自体も呼び出すことができ、代わりの構文として (language :name) を使用した方が理解しやすければ、この構文を使用することもできます。このように、Clojure は呼び出し可能な機能としての記述が豊富であるため、繰り返される構文 (Java プログラムの世界では至るところで見られる getset など) が削減され、この言語を使いやすいものにしています。

しかしマップは、JVM のクラスをまるごとエミュレートするわけではありません。Clojure には、問題をモデル化するのに有用な別の方法として、データと振る舞いを両方とも含んだ上に、JVM とよりシームレスに統合する方法が用意されています。これにより、似たような JVM クラスにさまざまな完全度で対応する (型とレコードを含んだ) いくつかの構成体を作成することができます。(deftype ) を使用すると、(従来は、メカニカルな構造をモデル化するために使用していた) 型が作成されます。例えば、XML を保持するためのデータ型が必要であるならば、XML 内に埋め込まれているデータを抽出するためのメカニズムとして、おそらく (deftype MyXMLStructure) を使用することでしょう。Clojure におけるレコードは、データに対してイディオムのように (アプリケーションの目的を果たす上でコアとなる情報のレコードとして) 使用されます。この使用方法をサポートするために、Clojure では呼び出し可能な機能をはじめとする各種の機能を含む多数のインターフェースをそのレコード定義の中に自動的に含めます。リスト 2 に示す REPL での操作は、レコードのクラスとスーパークラスを扱っています。

リスト 2. レコードのクラスとスーパークラス
user=> (defrecord Person [name age postal])
user.Person

user=> (def bob (Person. "Bob" 42 60601))
#'user/bob
user=> (:name bob)
"Bob"
user=> (class bob)
user.Person
user=> (supers (class bob))
#{java.io.Serializable clojure.lang.Counted java.lang.Object 
clojure.lang.IKeywordLookup clojure.lang.IPersistentMap 
clojure.lang.Associative clojure.lang.IMeta 
clojure.lang.IPersistentCollection java.util.Map 
clojure.lang.IRecord clojure.lang.IObj java.lang.Iterable 
clojure.lang.Seqable clojure.lang.ILookup}

リスト 2 では、nameagepostal をフィールドとして持つ Person という名前の新規レコードを作成しています。このようなタイプの新規レコードを作成するには、Clojure でコンストラクターを呼び出すための構文糖を使用します (関数呼び出しとして、クラス名にピリオドを続けて使用します)。戻り値は、名前空間付きのインスタンスです。(REPL でのすべての操作は、デフォルトで user 名前空間で行われます。) 呼び出し可能な機能のルールは保持されているため、リスト 1 に示したような構文糖を使用して、レコードのメンバーにアクセスすることができます。

(class ) 関数を呼び出すと、Clojure が作成した (Java コードとの相互運用性がある) クラス名を名前空間に続けた値が返されます。(supers ) を使用すると、Person レコードのスーパークラスにもアクセスすることができます。リスト 2 の末尾の 5 行では、(Clojure のネイティブ構文を使用して、マップがクラスとオブジェクトを扱えるようにする) IPersistentMap などの呼び出し可能な機能のインターフェースを含む、いくつかのインターフェースを Clojure が自動的に実装しています。レコードでは自動的に一連のインターフェースが読み込まれるのに対し、型では自動的にインターフェースが実装されることがない点が、レコードと型との重要な違いの 1 つです。


レコードを使用したプロトコルの実装

Clojure のプロトコルは、名前が指定された関数とそのシグニチャーの名前付きセットです。リスト 3 の定義では、プロトコル・オブジェクトと一連のポリモーフィックなプロトコル関数を作成しています。

リスト 3. Clojure プロトコル
(defprotocol AProtocol
  "A doc string for AProtocol abstraction"
  (bar [this a] "optional doc string for aar function")
  (baz [this a] [this a b] 
     "optional doc string for multiple-arity baz function"))

リスト 3 の関数では、最初の引数の型に基づいてディスパッチが行われ、その型に関してポリモーフィックな関数になっています (Java のコンテキスト・ホルダーを模倣するために、以前から最初の引数には this が指定されています)。そのため、すべてのプロトコル関数が少なくとも 1 つの引数を取る必要があります。従来、プロトコルにはキャメルケースで名前が付けられています。プロトコルは JVM レベルで Java インターフェースを具象化するため、Java の命名規則に合わせておくと相互運用が容易になるからです。

Java 言語でインターフェースを実装するのとまったく同じように、レコードはプロトコルを実装することができます。プロトコルを実装したレコードは、プロトコルのシグニチャーと一致する関数を (ランタイム・チェックとして) 実装する必要があります。リスト 4 では、AProtocol を実装するレコードを作成しています。

リスト 4. プロトコルの実装
(defrecord Foo [x y]
   AProtocol
   (bar [this a] (min a x y))
   (baz [this a] (max a x y))
   (baz [this a b] (max a b x y)))

;exercising the record
(def f (Foo. 1 200))
(println (bar f 4))
(println (baz f 12))
(println (baz f 10 2000))

リスト 4 で作成しているレコードは、xy という 2 つのフィールドを持つ Foo という名前のレコードです。プロトコルを実装するには、プロトコルのシグニチャーと一致する関数を含める必要があります。プロトコルを実装した後は、この関数をオブジェクトのインスタンスに対する正規の関数として呼び出すことができます。関数定義の中では、レコードの内部フィールド (x および y) と関数の引数にアクセスしています。


プロトコルによる拡張の方法

プロトコルは、既存のクラスと階層を綺麗に拡張する方法として、Expression Problem を念頭に置いて設計されました (Expression Problem の完全な定義については、前回の記事を参照してください)。この拡張は (Clojure の他のすべてのものと同じく) 関数であるため、オブジェクト指向言語に特有の識別子と継承の問題の多くが現れない上に、このメカニズムによって有用な各種の拡張が実現されます。

Clojure はホストされる言語です。つまり、(ClojureScript コンパイラーを利用することで) .NET や JavaScript をはじめとする複数のプラットフォーム上で (プロトコルを使用して) 実行されるように設計されています。JavaScript には、コードをセットアップ、破棄、ロード、評価することができる環境が必要です。そのため ClojureScript では BrowserEnv レコードを定義し、どのような JavaScript 環境 (ブラウザー、REPL、あるいは疑似環境) が適している場合でも、その環境を対象とした setupteardown などのライフサイクル関数をこのレコードが扱うようにしています。BrowserEnv のレコード定義はリスト 5 のとおりです。

リスト 5. ClojureScript の BrowserEnv レコード
(defrecord BrowserEnv []
  repl/IJavaScriptEnv
  (-setup [this]
    (do (require 'cljs.repl.reflect)
        (repl/analyze-source (:src this))
        (comp/with-core-cljs (server/start this))))
  (-evaluate [_ _ _ js] (browser-eval js))
  (-load [this ns url] (load-javascript this ns url))
  (-tear-down [_]
    (do (server/stop)
        (reset! server/state {})
        (reset! browser-state {}))))

IJavaScriptEnv プロトコルの中で定義されているライフサイクル・メソッドによって、インプリメンター (ブラウザーなど) は一般的なインターフェースにアクセスすることができます。各関数名の前に付けられたハイフン (例えば (-tear-down )) は、ClojureScript の規約です (Clojure の規約ではありません)。

Expression Problem に対するソリューションのもう 1 つの目標は、既存の階層に新しい機能を追加しても、再コンパイルしたり、それらの階層に手を加えたりする必要がないようにすることです。Clojure のバージョン 1.5 では、「リデューサー」と呼ばれる先進的なコレクション・ライブラリーが導入されました。このライブラリーは、多くのコレクション型を対象とした自動並行処理を追加します。既存の型がリデューサーを利用するには、ライブラリーのメソッドの 1 つである coll-fold を実装する必要があります。プロトコルと便利な extend-protocol マクロ (このマクロは、プロトコルを一度に複数の型に拡張できるようにします) を使用すると、(coll-fold ) 関数が魔法のようにいくつかのコアとなる型で使用できるようになります (リスト 6)。

リスト 6. (coll-fold ) を複数の型で使用できるようにするリデューサー
(extend-protocol CollFold
 nil
 (coll-fold
  [coll n combinef reducef]
  (combinef))

 Object
 (coll-fold
  [coll n combinef reducef]
  ;;can't fold, single reduce
  (reduce reducef (combinef) coll))

 clojure.lang.IPersistentVector
 (coll-fold
  [v n combinef reducef]
  (foldvec v n combinef reducef))

 clojure.lang.PersistentHashMap
 (coll-fold
  [m n combinef reducef]
  (.fold m n combinef reducef fjinvoke fjtask fjfork fjjoin)))

リスト 6(extend-protocol ) を呼び出せば、((coll-fold ) という 1 つのメソッドを含んだ) CollFold プロトコルを nilObjectIPersistentVectorPersistentHashMap の各型で使用できるようになります。このライブラリーを使用すると、空のコレクションという共通のエッジ・ケースが処理されて、nil (Java 言語の null の Clojure 版) でさえも正常に動作します。リデューサー・ライブラリーはまた、2 つのコアとなるコレクション・クラスである、IPersistentVector および IPersistentHasMap にも働きかけ、これらのコレクション階層の最上位近くにリデューサー機能を追加します。

Clojure では、洗練された一連のビルディング・ブロックを使用して、シンプルでありながらも強力な拡張を実現します。この言語はクラス・ベースではなく関数ベースであることから、クラスを使用しないコード編成が編成原則の中心となるため、一部の開発者は悪戦苦闘しています。Java にはパッケージ、クラス、メソッドがある一方で、Clojure にあるのは、名前空間 (大まかに言えば、パッケージに相当します) と関数 (大まかに言えば、メソッドに相当します) である点が Clojure と Java 言語では異なりますが、それ以外のコード編成は、ほぼ同じです。Clojure のプロトコルはまた、必要に応じてネイティブ Java インターフェースを生成し、開発者はこのインターフェースを相互運用のために使用することができます。Clojure の規約では、コンポーネント境界でプロトコルを定義し、似たような関数とプロトコルを 1 つの名前空間の中に配置します。Clojure には、情報を隠す仕組みとしてのクラスがありませんが、名前空間のプライベート関数を ((defn- ) 関数定義を使用して) 定義することができます。

Clojure では名前空間の中でコードを編成するため、拡張を整理して集約した配置にすることが可能となります。リスト 6CollFold プロトコルを例にとると、このプロトコルは Clojure のソースの reducers.clj ファイル内にあります。プロトコル、新しい型、拡張は、すべて Clojure 1.5 で追加されたこのファイルの中にあります。プロトコルによる拡張を使用すると、コアとなる型 (Object など) にアクセスしてリデューサー機能を追加することができます。この機能の一部は、名前空間のプライベート関数によって reducers 名前空間内に実装されます。Clojure は、重要な新しい振る舞いを既存の階層に首尾よく追加しますが、複雑な処理もなく、極めて正確に行います。また、関連するすべての詳細は 1 箇所に保持されます。

(extend-type ) マクロは、(extend-protocol ) マクロと似ています。(extend-type ) マクロを使用すると、いくつかのプロトコルを 1 つの型に同時に追加することができます。リスト 7 には、ClojureScript によってコレクション機能を array に追加する方法を示します。

リスト 7. JavaScript の配列へのコレクション機能の追加
(extend-type array
  ICounted
  (-count [a] (alength a))

  IReduce
  (-reduce [col f] (array-reduce col f))
  (-reduce [col f start] (array-reduce col f start)))

リスト 7 では、ClojureScript は (count )(reduce ) といった Clojure の関数に対応するために JavaScript の配列を必要としており、(extend-type ) マクロによって複数のプロトコルを 1 箇所で実装できるようにしています。Clojure では、コレクションが length ではなく count に対応するように要求するため、ICounted プロトコルと関数を使用できるようにすることによって適切なメソッドのエイリアスを追加しています。

プロトコルの具象化にレコードは必要ありません。Java における無名オブジェクトのように、プロトコルは具象化してインラインで使用することができます (リスト 8)。

リスト 8. プロトコルのインラインでの具象化
(let [z 42
      p (reify AProtocol
       (bar [_ a] (min a z))
       (baz [_ a] (max a z)))]
  (println (baz p 12)))

リスト 8 では、let ブロックを使用して 2 つのローカル・バインディング (z および p) とインラインのプロトコル定義を作成しています。無名プロトコルの作成では、まだローカル・スコープにアクセスすることができ、引数としての z の存在が有効です。これは、zlet ブロックのスコープ内にあるからです。このように、具象化されたプロトコルは、そのプロトコルの環境をクロージャー・ブロックのように囲みます。ここではプロトコルを完全には実装していないことに注意してください。baz 関数の引数の個数が異なるバージョンが一部ありません。Java のインターフェースとは異なり、プロトコルの実装はオプションです。Clojure はコンパイル時にプロトコルを実行しませんが、存在しないプロトコル・メソッドを Clojure が要求している場合には、ランタイム・エラーを生成します。


まとめ

連載 Java.next の今回の記事では、クラスやインターフェースといった一般的な Java 言語の仕様を Clojure の構造にマッピングする方法について詳細に取り上げました。また、Clojure でのプロトコルのさまざまな使用方法を探り、Clojure が Expression Problem をシンプルかつ簡潔に解決する方法を複数の実際のバリエーションを用いて説明しました。次回の記事では、Groovy のミックスインを取り上げ、「継承を伴わない拡張」のミニシリーズを締めくくります。

参考文献

学ぶために

  • プロダクティブ・プログラマ ― プログラマのための生産性向上術』(Neal Ford 著、オライリー・ジャパン、2008年): コーディングの効率性を改善するためのツールとプラクティスについて説明している、Neal Ford の著書です。
  • Clojure: Clojure は JVM 上で実行される最近の関数型 Lisp です。
  • Scala: Scala は JVM 上で実行される最近の関数型言語です。
  • Groovy は、Java の構文と機能が更新された動的バージョンです。
  • The Expression Problem」: Philip Walder 氏の未発表の論文 (1998年) には、Expression Problem が詳しく説明されています。
  • Solving the Expression Problem with Clojure 1.2」(Stuart Sierra 著、developerWorks、2010年12月): Expression Problem に対する Clojure のソリューションについての説明を読んでください。
  • Java プラットフォーム用の代替言語を探る」: この Knowledge path では、JVM 用のさまざまな代替言語に関する developerWorks のコンテンツを紹介しています。
  • 言語設計者のノート」: この developerWorks の連載記事では、Java 言語のアーキテクトである Brian Goetz 氏が Java 言語の設計上の問題点の一部を解説しています。これらの問題は、Java SE 7、Java SE 8、およびそれ以降の Java 言語の進化に対する課題を提起しています。
  • ClojureScript: Clojure から JavaScript へのコンパイラーである ClojureScript について詳しく学んでください。
  • 連載「関数型の考え方」: developerWorks に公開された Neal Ford の連載記事を読み、関数型プログラミングの知識を深めてください。
  • Java.next: 継承を伴わない拡張、第 1 回: Groovy、Scala、Clojure でクラスに振る舞いを追加する方法を探る」(Neal Ford 著、developerWorks、2013年7月): Java.next 言語で Java クラスを拡張する方法としての、カテゴリー・クラス、ExpandoMetaClass、暗黙の型変換、プロトコルをはじめとする、Groovy、Scala、Clojure における拡張メカニズムを詳しく探ってください。
  • Java.next: Groovy、Scala、Clojure の共通点、第 1 回」(Neal Ford 著、developerWorks、2013年3月): Java 言語では演算子を多重定義できない点に関し、Java.next 言語 (Groovy、Scala、Clojure) ではどのように対応しているかを説明しています。
  • Java.next: Groovy、Scala、Clojure の共通点、第 2 回」(Neal Ford 著、developerWorks、2013年4月): Java.next 言語では便利な構文が導入され、定型的な処理や複雑さが減少していることを説明しています。
  • Java.next: Groovy、Scala、Clojure の共通点、第 3 回」(Neal Ford 著、developerWorks、2013年5月): 例外、文と式の比較、そして null に関するエッジ・ケースについて、Groovy、Scala、Clojure で改善されている点を比較しています。
  • Java.next: Java.next 言語」(Neal Ford 著、developerWorks、2013年1月): この Java.next 言語とその特長について概要を説明する記事では、3 つの次世代 JVM 言語 (Groovy、Scala、Clojure) の類似点と相違点を詳しく探ります。
  • この著者による他の記事 (Neal Ford 著、developerWorks、2005年6月から現在まで): Groovy、Scala、Clojure、関数型プログラミング、アーキテクチャー、設計、Ruby、Eclipse、その他の Java 関連の技術について学んでください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した豊富な記事を調べてください。

製品や技術を入手するために

  • IBM 製品の評価版をダウンロードして、DB2、Lotus、Rational、Tivoli、WebSphere が提供するアプリケーション開発ツールやミドルウェア製品を試してみてください。

議論するために

  • developerWorks コミュニティーに加わってください。ここでは他の developerWorks ユーザーとのつながりを持てる他、開発者によるブログ、フォーラム、グループ、Wiki を調べることができます。

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=942840
ArticleTitle=Java.next: 継承を伴わない拡張、第 2 回
publish-date=09052013