メタプログラミング Ruby まつもとの直伝プログラミング

目次

メタプログラミングRuby / 毎回の講義 / OOへ至る道 / ruby入門 / 講義ドキュメント

連載原稿

まつもと直伝 プログラミングのオキテ 第1回(1)

Rubyを題材にオブジェクト指向という考え方について説明

(ネットワーク応用通信研究所 まつもと ゆきひろ)

奪取の構造

コンピュータもいろいろと手がかかります。

  • システムを設計するのも大変
  • バグの発生
  • 仕様変更

主従関係が逆転し、「コンピュータのドレイ」となる

目指せ、プログラマの復権。

権利を勝ち取る戦いには「武器」が必要です。それがこの連載で解 説する「言語」と「テクニック」です。

復権の武器

「武器」である「プログラミング言語」と、その言語を使った「プログラミン グ・テクニック」を解説します。

プログラミング言語は、プログラム(処理手順)を記述する方法で、世の中 にはたくさんあります。有名なものとしてはBASIC、FORTRAN、C、C++、Java、 Perl、PHP、Python、Rubyなどがあります。

言語にはそれぞれ特徴や性質、長所・短所、向き・不向きがあり、プログラム の書きやすさ(生産性)もずいぶん違うからです。

ある研究によると、プログラム開発に用いるプログラミング言語の種類と、プ ログラミングの生産性にはあまり相関がなく、一定期間に開発できるプログラ ムの規模はプログラミング言語によらず(ある程度)一定なのだそうです。

また、別の研究によれば、同じタスクを達成するためのプログラミングの規模 はプログラミング言語や利用するライブラリによって最大で数百から数千倍の 差が発生するのだそうです。このことから、適切な言語を開発すれば、あなた も数千倍デキるプログラマになれる可能性があるというわけです。

何にでも代償は必要で、プログラム開発効率が高い環境は、しばしば実行時の 効率が低いことが多いのです。

プログラミングとオブジェクト指向の関係

いかに書くか

プログラミング・テクニックのこと

コーディング・スタイルは、プログラムの書き方の細かい部分、 例えば変数名の選び方や、関数の書き方などを含みます。

アルゴリズムは問題を解決する手順のことです。世の中にはさまざまなア ルゴリズムが知られています。プログラムはアルゴリズムの具体化と言っ ても過言ではありません。

いくつかのアルゴリズムは、自分の力だけで考え出すのは 困難です。例え ば、配列要素を大きさの順に並べ替えるソートのアルゴリズムは数多く知 られていますが、既存のアルゴリズムを全く知らなければ高速なソートは 実現できないでしょう。アルゴリズムはしばしば特定のデータ構造を伴い ます。かつて「プログラミングとはアルゴリズム+データ構造である」と 断言した偉い人もいるほどです*3。

デザイン・パターンとは、ソフトウエア設計に際して利用できるデータ構 造などの設計ノウハウに名前を付けて、カタログ化したものです。アルゴ リズムやデータ構造も広い意味ではデザイン・パターンに分類できます。 有名なものが23個あります*4。

開発手法は、

  • プログラム開発プロセス全体
  • 規模が大きくなり、関連する人員が増加すると、導入が必要

オブジェクト指向プログラミング

いまやオブジェクト指向はプログラミングにおける常識になりつつあり、 今後その重要性が下がることはちょっと考えられないと思います。プログ ラミングを学ぶにあたって、オブジェクト指向という考え方を理解するこ とは大変重要なのです。

しかしながら、プログラマを目指す人々の中には、「オブジェクト指向は 難しい」とか、「なかなか分からない分からない」という印象を持つ人が 多いようです。そこで今回から数回かけて、オブジェクト指向という考え 方、オブジェクト指向プログラミングというプログラミング・テクニック について、丁寧に解説してみようと思います。

オブジェクト指向の難しさ

  • イメージがつかみにくい
  • 実際に使おうとするとどうしたらよいのか分からない

    もう一つ重要な点は、「オブジェクト指向プログラミング」と一言で表し ても、実際には複数のプログラミング・テクニックが集まっていることで す。複数のテクニックをいっぺんに理解しようとすると、なかなか難しい ので、一つひとつ分解して考えてみましょう。

    個人的な意見ではオブジェクト指向プログラミングを構成するテクニック の中で最も重要なテクニックは「ポリモーフィズム」あるいは「多態」と 呼ばれるものです。今月はまずポリモーフィズムから解説します。

まつもと直伝 プログラミングのオキテ 第1回(3)

ポリモーフィズム

複数の形態であること,多態, 多相

言い換えると,複数の種類のものをあたかも同じものであるかのように扱うこ とができること

ここに3つの箱があります。箱にはそれぞれふたがあり、ふたが乗せてあるだけ のもの、鍵が付いたもの、リボンで結んであるものに分かれます。箱自体が非 常に高価なので、それぞれの箱には専任のオペレータが付いていて、箱に対し てなにかしたいときには、オペレータに命令します(図1[拡大表示])。

3種類の箱はそれぞれ開け方が違っています。しかし「ふたを開けろ」と命令す れば、それぞれの箱の開け方を知っている専任のオペレータが命令を実行して くれます。ですから、種別が違うこれら3種類の箱を同じように「箱であって、 ふたを開けることができるもの」として取り扱うことができます。これこそが ポリモーフィズムの本質です。

プログラミングでは「箱を開けろ」という命令のことを「メッセージ」、それ ぞれの種別に応じた具体的な箱の開け方を「メソッド」と呼びます。

実際のプログラム

上の例題を表現するプログラムを図2[拡大表示]に示します(別掲記事「今回使っ たRubyの文法」を参照)。

box_openという手続きが上の例題における「専任のオペレータ」だと考えてく ださい。box_openを呼び出すと、パラメータ(かっこの中の値)の箱の種類に 応じたやり方で箱を開けます。ただし、ここでは「ふたを開けます」と表示す ることで、実際に開けたとしています。この「種別に従って適切な処理を進め る」ことがポリモーフィズムの基本です。

しかし、図2だけでは十分とはいえません。box_openという手続きを自分で定義 することを考えてみましょう。単純にこの手続きを実現しようと思うと、図 3[拡大表示]のようになるでしょう。

しかし、図3のコードはあまりうれしくありません。新しく箱の種別が増えるた びにbox_open手続きを書き直す必要がありますし、box_openのほかにも箱の種 別に応じてポリモーフィズムが適用される手続きがたくさんあったりしたら、 プログラムをあちこち修正しなければならなくなり、種別の追加はげっそりす るほどの手間になってしまうでしょう。

修正個所が増えれば増えるほど間違いが入り込む危険性が高まりますし、結果 として正しく動かないプログラムができ上がってしまう可能性が増えます。

このような修正を人間が直接こなすべきではありません。データの種別に応じ て適切な処理(メソッド)を選ぶ動作は、道具であるプログラミング言語が対 応するべきです。そのような支援があってこそ十分なポリモーフィズムと呼べ るでしょう。

さて、そのような十分なポリモーフィズムの働きを見るための下準備として図 2のプログラムを変形してみましょう(図4[拡大表示])。

図4のプログラムでは、パラメータが先頭に移動して「.」が追加されています。 このコードを「先頭の式の値に対してopenというメッセージを送る」と解釈し ます。つまり、「先頭の式の値の種類に応じたopenという手続きを呼び出す」 というように振る舞います。よりポリモーフィズムを意識した呼び出し方になっ ています。

図4のプログラム中のそれぞれに応じた処理であるメソッドの定義は図5のよう にしました。

図5[拡大表示]のプログラムでは、3種類の箱、box1、box2、box3のそれぞれに 直接「どうやって開くか」という方法を教えています。

図5のプログラムと図3のプログラムを比べてみると、明示的な条件判断が消え て、すっきりしているのが分かります。しかも、ただ簡潔なだけではありませ ん。図5のプログラムでは新しい箱の種別、例えば「スライドさせて開ける箱」 が導入されたとしても、既存のプログラムに一切手を入れる必要がありません。 手を入れる必要がないということは、間違いを仕込んでしまう危険性もないと いうことです。

ポリモーフィズムのありがたさ

ポリモーフィズムについて説明しましたが、ポリモーフィズムがあると何がう れしいのでしょう。

まず第1にいろいろな種類のデータを統一的に扱うことができるので、プログラ ムがHow(どのように処理するか)ではなく、What(なにを行いたいか)に集中 できます。そのため、プログラムの意図が細かな処理に埋没せず、読みやすく 簡潔なプログラムを実現できます。

第2に対象となるデータに応じて自動的に最適な処理が選択されますから、プロ グラム内部での不整合が起きません。鍵付きの箱をリボン付きの箱を開ける手 続きに渡しておかしなことが起きることを心配しなくても済みます。誤って間 違いを組み込んでしまわないことは、プログラムの開発において大変重要です。 プログラマの負荷を減らすことにもつながります。

第3に新しいデータへの対応を簡単に追加できますから、プログラムの拡張性が 高まります。

このように、ポリモーフィズムはプログラム開発の効率を高めてくれるのです。 オブジェクト指向プログラミングを構成するテクニックのうち、ポリモーフィ ズムは最も重要なものと言えます。

第2回 抽象データと継承

オブジェクト指向プログラミングを構成する3原則

  • ポリモーフィズム
  • データ抽象継承

ポリモーフィズムを 動的結合, データ抽象情報隠ぺい, カプセル化 ともいう.

Javaの誕生

Cとの互換性を強調したC++には、低レベルなことも記述できるというメリット と、低レベルなことも記述しなければならないというデメリットが共存してい ました。

その点を改善するべく1990年代になって登場したのがJavaです。JavaはC++から Cへの互換性(とそれに伴う制約)を取り除き、その代わりにLispなどからより 良い機能をいくつか取り込んで誕生しました。

また、JVM(Java Virtual Machine)という仮想マシンを経由することで、1つ のプログラムが再コンパイルなく、あらゆるプラットフォームで動作するとう たわれていました。

今やJavaは1990年代に誕生した言語の中では最も成功した言語として世界中で 広く用いられています。

オブジェクト指向分析・設計技法

UML(Unified Modeling Language)はオブジェクト指向アプローチで設計され るソフトウエアのモデルを記述するための記法であり、またそれを用いた分析・ 設計の方法論でもあります。

UMLは信頼性の高いソフトウエアをオブジェクト指向を用いて設計する際に有益 な方法ではありますが、UML全体はそれなりに複雑で周辺の概念の数も多く、初 心者には難しいという印象を与えてしまいました。

● その2

複雑さという敵

ソフトウエア開発の最大の敵は複雑さです。

ソフトウエアへの要求は増し、開発されるソフトウエアはどんどん複雑になっ てきています。

年々性能が向上するコンピュータの処理能力の限界よりも、人間の理解力の限 界によるソフトウエア生産性の限界の方が、より厳しい制約になってきていま す。これだけコンピュータが高速になった現在、多少性能が劣っても、より複 雑で、より大規模なソフトウエアをより速く開発できることが期待されている のです。

構造化プログラミングの意味

プログラムの制御の流れ、つまり実行順序を「順接」、「分岐」、「反復」の 3種類だけに制限し、かつ共通する処理をサブルーチンとしてくくり出すことを 基本とした考え方です(図1[拡大表示])。

構造化プログラミングが取った「制約」と「抽象化」こそ、ソフトウエアの複 雑さを人間が取り扱える範囲内に抑えるために非常に有効な手法なのです。

自由度と複雑性は減っても記述力は減っていないことになります。

抽象化の目的はいくつかのまとまりに名前を付けることで、内部の詳細を気に せずに取り扱うことです。「ブラックボックス化」とも言います。ブラックボッ クスでは入力と出力だけが決まっていて内部の処理を隠しています*2。

ブラックボックスの内部を含めて考えるとシステム全体の複雑さは変化してい ません。しかし、内部を考えなければ、システムの複雑さを人間の取り扱える 範囲に抑えることができます。

さらに、内部が隠されているということは、入力と出力が同じであれば、内部 の処理の方法をどのように変更しても外部に影響がない、つまり、将来の変更 に対して強いことを意味します。ソフトウエアに変更はつきものですから、こ のような変化に強い性質は大変望ましいことです。

データも抽象化

オブジェクト指向プログラミングは、データによる複雑さへの対抗手段として 登場した

抽象データとは、データと手続きをまとめたものです。データの中身は所定の 手続きを通さないと見えません。データとその取り扱い方法をまとめてブラッ クボックス化できるわけです。

このスタックの操作をRubyで記述

それに対して図4のプログラムでは実装の変更に伴い、必ずプログラムの修正が 必要になります。スタックを利用している場所すべてを変更しなければならな いので、プログラムの規模が大きくなればなるほど変更が大変になります。こ れではプログラムを改善するためであっても、スタックの実装を変更したくな いと尻込みしてしまうでしょう。「変化に強い」というのは抽象データの大き な利点です。

もう一つの利点は、処理のイメージがつかみやすいことです。例えばスタック にデータをプッシュする操作は、図3では次のように表現しています。

stack.push(5)

 一方、図4では以下のように記述しています。

stack[sp] = 5 sp += 1

図3の方が「スタックにプッシュする」という操作が直接的に表現できています。 データを操作する側は、図4のような処理の詳細ではなく、むしろ「何をしよう しているか」に興味があります。そのため、処理の詳細が隠される抽象データ の方がコードが明確になり、目的にかなうのです。

イメージしやすいのは個別の操作だけではありません。抽象データは「特定の 操作に反応するインテリジェントなデータ」としてとらえることができます。 各種の刺激に応じて反応を示す現実世界の実体との関連付けが容易になるとい うメリットがあります。

抽象データにより、プログラムがとりあつかうデータが単なる数値や文字列の ようなあまり具体性のないものから、人間の頭脳がイメージしやすいより具体 的なものに変化します。コードの「抽象化」によりイメージが「具象化」して いるわけです。このようなインテリジェントなデータを、現実世界の実体(も の)との対応から、しばしば「オブジェクト」と呼び、オブジェクト指向プロ グラミングの名前の由来となっています。

● その3

ひな形

プログラム中にオブジェクトが登場する場合、同じ動作をするものが多いでしょ う。交通シミュレーション・プログラムでは車を表すオブジェクトや信号を表 すオブジェクトが数多く登場します。同じ種別のオブジェクトはみな同じ性質 を持っていますが、位置や色などそれぞれ状態が異なります。

抽象化の原則から言えば、同じものが数多く登場する場合にはグループ化して まとめてしまうべきです。このことをDRY(Don't Repeat Yourself)原則と呼 ぶこともあります。

既に見てきたように、プログラムの重複は諸悪の根源です。重複があるとプロ グラムの修正が広範囲に及びますから、修正コストが高くなります。変更カ所 が複数に及び、そのうちのたった1つでも修正を忘れてしまうとプログラムは正 常に動作しません。重複はプログラムの信頼性を低下させる可能性が高いとい えます。

さらに言えば、重複のあるプログラムは冗長ですから、人間が読む時にプログ ラムの「意図」を解釈するコストも増大します。コードの重複が多い図4のプロ グラムが、重複のない図3のプログラムよりも意図を理解しにくかったことを思 い出してください。コンピュータはプログラムが読みにくいかどうか、重複が あるかどうかなど気にしません。しかし、人間の方はプログラム開発する間、 数え切れないほどプログラムを読み、解釈し、心の中で挙動を想像しています。 ですから、プログラムが人間にとって読みやすいかどうかは生産性に直結しま す。重複が多く冗長で読みにくいプログラムはそれだけで生産性を低下させま す。プログラミング中のにコピー・アンド・ペーストを繰り返すのは、重複部 分を増やすために推奨されない行為だと言えるでしょう。

話題をオブジェクトに戻しましょう。同じ種類のオブジェクトがたくさん存在 している場合、重複を避けるため同種のオブジェクトをまとめる方法は大きく 分けて2つあります。

一つはクローン型です。元になるオブジェクトのコピーを作ることで、同種の オブジェクトを作り出します。実はオブジェクト指向プログラミング言語では クローン型は少数派です。SelfやIoなどの言語が採用しています。少々意外な ことにJavaScriptもクローン型です。今や最も有名なクローン型オブジェクト 指向言語かもしれません。 もう一つはひな形を用意する方法です。例えるな らば、たい焼きの型とたい焼きの関係でしょうか。型があれば同じ形のたい焼 きをたくさん作ることができます。このひな形にあたるものをオブジェクト指 向プログラミングでは「クラス」と呼びます。同じ種別のオブジェクトはそれ ぞれ同じクラスに属し、操作や性質を共有します。

クローン型と違って、クラス型オブジェクト指向言語はひな形となるクラスと ひな形から作られたオブジェクトを明確に区別します。たい焼きの型はたい焼 きそのものではないことと同じです。クラスとオブジェクトの関係についてさ らに例を挙げると、整数クラスと数字の「1」オブジェクト、犬クラスと特定の 「ポチ」オブジェクトなどがあるでしょう。クラスと対比し、違いを際立たせ るためにオブジェクトのことをしばしばインスタンスと呼びます。呼び方が違 うだけでこれまで説明したオブジェクトと同じものを指します。

クラス型オブジェクト指向言語であるRubyでは*5、クラスを定義する際に 「class文」を使います。図3で登場したStackも、実はStackクラスです。 Stackクラスの定義を図5に示しました。

classの後ろに書かれているのがクラス名です。図5[拡大表示]では、Stackです。 Rubyではクラス名の先頭の文字を大文字にするというルールがあります。 classから対応する(末尾の)endまでがクラスの定義になります。ここでは3つ の手続き、initialize、push、popが定義されています。

initializeは図3の2行目で呼び出されます。

stack = Stack.new

スタックが作られる度に呼ばれる初期化のための手続きです。

図5では、@stack(スタックの実体となる配列)と@sp(配列のインデックス) という2つの変数を初期化しています。Rubyでは「@」で始まる変数はオブジェ クトごとに独立した値を持つ変数で、インスタンス変数と呼ばれます。複数の スタックを作った場合には、それぞれ別の@stackと@spを持つことになります。

pushとpopはスタック操作の手続きです。図4にあるスタック操作を手続きにま とめただけのものです。

図5のinitializeのようにクラスに対して定義されたオブジェクト内部の操作手 続きを「メソッド」と呼びます。

なお、説明を単純化するため、図5の例では領域チェックなどは行っていません。 実際にはインデックスが負にならないかどうかなどのチェックが必要になるで しょう。

似た部分をくくり出す継承

 ソフトウエアの規模が拡大し、中に含まれるクラスが多くなってくると似た 性質を持つクラスが複数登場します。既に見てきたように同じことを何度も繰 り返すのはDRY原則に違反します。無駄ですし、理解の妨げにもなります。変更 のコストが高くなり、生産性も下がります。ですから、似たような性質を持つ クラスが複数あるなら、オブジェクトと同様にクラスについても似た部分をく くりだする「仕掛け」があれば良いでしょう。

「継承」とは似た部分をくくり出す仕掛けです。具体的には、継承はあるクラ スの性質を受け継いだ新しいクラスを作る機能です。元になったクラスのこと をスーパークラス、新しく作られるクラスのことをサブクラスと呼びます。サ ブクラスはスーパークラスのすべてのメソッドを受け継いでいますし、必要で あれば新しいメソッドを追加できます。さらに受け継いだメソッドを自分の要 求に合わせて置き換えることもできます。

図6[拡大表示]に、図5で定義したStackクラスを継承したFixedStackクラスを示 しました。class文のクラス名の後ろにある「< Stack」でスーパークラスを指 定しています。これはFixedStackクラスがStackクラスのサブクラスで、メソッ ドなどのStackクラスの性質をそのまま受け継ぐことを意味しています

FixedStackクラスではinitializeメソッドとpushメソッドが置き換えられてい ます。それぞれの定義の中で「super」を呼び出していますが、これは、スーパー クラスの同じ名前のメソッド(super)を呼び出すことを意味します。このよう な仕組みによってスーパークラスのメソッドの中身に立ち入ることなく、サブ クラスだけでメソッドの動作を変更できます。

initializeメソッドはオブジェクトの初期化のときに呼ばれますから、以下の ように書けば、initializeメソッドが10を引数として呼び出され、10が要素数 の上限としてインスタンス変数@limitに設定されます。

stack = FixedStack.new(10)

図6では、末尾にスタックの先頭要素を取り除くことなく、単に参照するための topメソッドを追加しています。スーパークラスに備わっていないメソッドを追 加した例と考えてください。

図6のように既存のクラスを利用して新しいクラスを作り出すことを「差分プロ グラミング」と呼びます。抽象化によって共通部分をスーパークラスとしてく くり出すことと、既存のクラスを利用して新しいクラスを作ることは同じ手法 の両面のようなものです。前者をボトムアップ・アプローチ、後者はトップダ ウン・アプローチと呼びます。

さて、Rubyをはじめとする多くの言語では1つのクラスに対して、1つのスーパー クラスが決まります。このような継承を「単純継承」と呼びます。継承につい て、クラスを拡張するトップダウン・アプローチから考えると、1つのクラスか らそれを拡張した別のクラスを作るということは、ごく自然なことです。

しかしながら、これまで見てきたような共通部分をくくり出すというボトムアッ プ・アプローチから考えると、1つのクラスに1つのスーパークラスというのは かなり厳しい制約です。実は、C++やLispなど複数のスーパークラスを持つこと ができる言語もたくさんあります。そのような継承を「多重継承」と呼びます。 多重継承には利点はもちろん、欠点もあります。多重継承については次回扱う ことにします。

多重継承の光と影 第3回(1)

2005/09/20

  • ネットワーク応用通信研究所 まつもと ゆきひろ
  • 出典:日経Linux 2005年7月号  125ページより

(記事は執筆時の情報に基づいており、現在では異なる場合があります)

多重継承では,個々のクラスが複数のスーパークラスを持つ。

ReadWriteStreamのスーパークラスがWriteStreamに限定されてしまう。

概要

今回は継承についてより深く学びます。特になぜ多重継承が必要なのか、多重 継承にはどのような問題があり、JavaやRubyがどのように解決しているのかを 学びます。

なぜ多重継承が必要なのか

単一継承はスーパークラスを1つしか持てません。これでは制限が厳しすぎると 感じることもあります。現実世界では、人はしばしば会社員であると同時に父 親であったり、プログラマであると同時にライターであったりします。

先月説明したように、継承をプログラムの共通部分をくくり出す抽象化の手段 として考えてみると、1つのクラスから抽象化(抽出)できる部分が1つだけと いうのはプログラミングの上で大きな制約になります。このような発想から生 まれたのが多重継承です。単一継承と多重継承の違いはスーパークラスの数だ けですから、多重継承は単純継承の完全なスーパーセットであり、自然な拡張 と考えることができます(図1[拡大表示])。

多重継承が使える言語は、単一継承にある不自然な制約を受けません。例えば、 単一継承のみを提供する言語Smalltalkの例で考えてみましょう。Smalltalkの クラス・ライブラリには単一継承によって不自然な形になっています。

Smalltalkでは、入出力を担うStreamクラスに3つのサブクラスがあります。入 力用のReadStream、出力用のWriteStream、双方向入出力向けの ReadWriteStreamです。ReadWriteStreamはReadStreamの機能とWriteStreamの機 能の両方を持ちます。しかし、Smalltalkは単一継承だけしか使えませんから、 両方のクラスから継承することはできません。

結局、ReadWriteStreamをWriteStreamのサブクラスにした上で、ReadStreamの コードをコピーし、ReadWriteStreamを実現しています(図2[拡大表示])。プ ログラムのメンテナンスという観点から、コードのコピーは避けるべきです。 言語の制約からコードをコピーが必要になるというのは望ましくありません。

一方、多重継承があれば、自然な発想に基づいてReadStreamクラスと WriteStreamクラスの両方を継承したReadWriteStreamを作れます(図3[拡大表 示])。

多重継承と単一継承は表裏一体

 このように多重継承と単一継承を比べて見ると、単一継承の特徴が際立ちます。

継承関係が単純

 単一継承では、継承関係が単純な木構造になります。これは利点でもあり、欠点でもあります。クラスの関係が単純なため混乱を生みませんし、実装も簡単です。しかし、先ほどのSmalltalkのStreamのように、継承関係を越えたコードの共有ができず、コピーが必要になる場合があります。

 式や変数に型指定が行われるJavaのような静的型の言語においては、単一継承からくるもう一つの欠点も見えてきますが、これは後で説明しましょう。

 多重継承の特徴はちょうど逆です。多重継承には2つの優れた特性があります。

  • 単一継承の自然な拡張
  • 複数のクラスから機能を取り込むことができる

 多重継承は単一継承ができることなら何でも実現できます。しかし、クラスの関係が複雑になりがちという欠点があります。

goto文と多重継承は似ている

 前回、構造化プログラミングについて学びました。gotoを用いて任意の場所 にジャンプできる構造よりも、分岐と繰り返しという制限された構造によって プログラムを構築した方が望ましいと説明しました。分岐や繰り返しはgotoを 使って実現できますし、分岐などでは直接実現できない制御の流れもgotoなら 記述できます。gotoは分岐などよりも「記述力が高い」ということができます。

 しかし、gotoは機能が高いにもかかわらず望ましくないと言われます。goto を使ったプログラムは、制御の流れがどこに移るのか一目で分からないことが 多く、人間にとって理解しにくいプログラムになりやすいのです。このような 制御が絡み合ったプログラムのことを「スパゲッティ・プログラム」と呼びま す。

 多重継承についても同じことが言えます。多重継承は単一継承の拡張ですか ら、単一継承を使ってできることはすべて実現できます。単一継承では解決が 難しい問題も解決できるでしょう。

 しかし、複数のスーパークラスからの継承を許すことで、クラスの関係が複 雑なネットワーク構造になります(単一継承は木構造)。このため、どのクラ スがどのクラスの機能を利用しているのか分かりにくくなりますし、問題が発 生したとき、どのクラスとどのクラスが悪さをしているか見分けにくくなりま す。

 このような絡み合った継承のことを俗に「スパゲッティ継承」と呼ぶことが あります。もちろん、多重継承があるからといって、必ずスパゲッティ継承に なるわけではありませんが、注意が必要なのは確かです。

多重継承の光と影 まつもと直伝 プログラミングのオキテ 第3回(2)

図4 多重継承の優先順位 メソッドを呼び出す優先順位がはっきりしない。

図5 親子関係にあるクラスの例 変数からはサブクラスにしかないメソッドDを呼び出せない。

多重継承から生まれる3つの問題

 もう少し、細かく見ていくと、多重継承の問題は以下の3点にまとまります。

  • 構成の複雑化

    単一継承では、あるクラスのスーパークラスは簡単に決まります。直接上の スーパークラス、そのスーパークラス、そのまたスーパークラス、…と一列 に並ぶ単純な関係です。多重継承では、あるクラスに複数のスーパークラス があり、その複数のスーパークラスそれぞれにさらに複数のスーパークラス があるので関係が複雑になってしまいます。

  • 優先順位

    複雑な関係を持つスーパークラスがあるということは、クラス群の優先順位が 一目で分からないということです。例えば図4[拡大表示]のようなクラス階層が あるとします。Dがあるメソッドを受け継ぐ順番は、D、B、A、C、Objectなのか、 D、B、C、A、Objectなのか、あるいは全く違う順序なのかが分かりません。一 つに決まらないのです。クラスの優先順位がはっきり定まる単一継承とは対照 的です。

  • 機能の衝突

    多重継承では複数のスーパークラスからメソッドなどの機能を受け継ぐこと から、受け継いだメソッドの名称が衝突することもあります。図4の例では、 クラスBとクラスCに同じ名前のメソッドがあった場合、どちらが有効になる のでしょうか。一意に定めることはできません。

多重継承の問題を解決するには

ここまで多重継承の欠点を説明しましたが、SmalltalkのStreamの例をはじめと して多重継承がなければ解決できない問題が残っています。

さらに、継承を抽象化の手段として考えるときには、なんからの形で多重継承 の役割を果たす機能が必要なのです。共通するクラスの機能をくくり出すとき に、1つのクラスにつき1つだけしか抽出できないと言う制限は厳しすぎるから です。

多重継承のメリットを享受しつつ、問題を避けたいというのであれば、なんと か問題を解決する機能を考えるしかないでしょう。構造化プログラミングが goto問題を解決した際の原則は、自由度の高いgotoの代わりに、gotoより制約 された3種類の制御構造を導入するというものでした。この3種類の制御構造は 制約されてはいますが、これらを組み合わせることで任意のアルゴリズムを記 述できます。これに従えば、より制約がきつい多重継承を導入すれよさそうで す。

そこで、これらの問題を解決あるいは軽減するために登場した「制約された多 重継承」とでも呼ぶべき機能が、Javaにおけるインタフェースであり、Lispや RubyにおけるMix-inです。ここからは、これらがどのような機能であり、どの ようにこれらの欠点を改善するのか見てみましょう。

静的型言語と動的型言語の違い

最初に、Javaのインタフェースを調べてみましょう。

インタフェースの仕組みを説明する前に、まずJavaのようなタイプのオブジェ クト指向言語と多重継承について説明しておきます。

オブジェクト指向言語は、大きく分けて「静的型言語」と「動的型言語」の2つ に分かれます。変数や式に型情報が付けられているJavaのような言語のことを 静的型言語と呼びます。

静的型言語では型が異なる値を変数に代入できません。代入するとコンパイル・ エラーになります。型の不整合はコンパイル時に見つかりますから、実行時に なって「型が合わない」というエラーは発生しません。実行しなくてもエラー を見付けられる、これが静的型言語のメリットの一つです。

String str;
str = "abc"; // 問題なし
str = 2; // コンパイル・エラー

オブジェクト指向言語では変数などの型をクラスで指定することが多いでしょ う。上の例ではStringクラスです。しかし、オブジェクト指向言語を使う際、 この例のように変数に特定のクラスのオブジェクト(そのクラスのインスタン ス)しか代入できないという制限は厳しすぎます。なぜなら、ポリモーフィズ ムが働く余地がありません。ある変数に、必ず変数と同じクラスのオブジェク トが入っているのなら、オブジェクトのクラスによってふさわしい挙動を選ぶ (ポリモーフィズム)ことはあり得ないからです。

静的型言語の特徴

そこで、静的型を持つオブジェクト指向言語では、あるクラスの変数にはその クラスのオブジェクトに加えて、サブクラスのオブジェクトが代入できるよう に設計されています。これによってポリモーフィズムが実現できるわけです。

図5[拡大表示]のプログラムを見てください。末尾に出てくる変数polyの型は Polygonですから、polyを介してPolygonクラスのメソッド(例えばメソッドC) を呼び出すことができます。しかし、実際に代入されているのはPolygonクラス のサブクラス*2であるRectangeクラスです。従って、呼び出されるのは Rectangeクラスで定義されているメソッドです。RectangeメソッドでPolygonク ラスのメソッドが再定義されていない場合は、そのままPolygonクラスのメソッ ドが呼び出されます。つまり、メソッドA'、B'、Cを呼び出せます。

しかしながら、polyはあくまでもPolygonクラスとしてプログラムに登場してい ますから、たとえRectangeクラスのオブジェクトが代入されていると(人間に は)明らかでも、poly変数を介する限りRectangeクラス固有のメソッド(メソッ ドD)を呼び出すことができません。

言い換えれば、変数は実際に代入されているオブジェクトをのぞき見る窓のよ うなものと言えます。変数に代入されているオブジェクトが持つメソッドがど のようなものであっても、ある変数を介してメソッドを呼び出すときには、そ の変数の型が「知っている」メソッドしか呼び出すことはできません。

メソッドDを呼び出してみると、静的型言語では無情にもコンパイル・エラーに なってしまいます。

これはRubyのような変数や式に型のない動的型の言語とは対照的です。これら の言語は変数を介して実際にオブジェクトのメソッドを呼び出してみて、見つ からなければはじめてエラーにしています。

動的型言語の特徴

動的型言語では継承関係に関係なくメソッドを呼び出せます。例えばRubyでは 要素を順番に取り出すメソッドeachが用意されていて、配列、ハッシュ、文字 列などにeachが備わっています。

obj.each {|x|
 print x
}

静的型言語では継承関係のあるメソッドしか呼べませんから、配列、ハッシュ、 文字列のすべてに対して呼び出しをかけられるのは、これらに共通するスーパー クラス(恐らくはObject)に所属するメソッドだけです。

これが、後ほど説明するとした静的型言語における単一継承の欠点です。

静的型言語では、クラス階層の木を横断してメソッドを呼び出したい場合、そ れらのオブジェクトすべてを表現できる「型」が必要です。そのような型がな ければ、メソッドが呼び出しできる範囲が非常に狭くなるのです。静的型のオ ブジェクト指向言語ではなんらかの形の多重継承が欠かせないことが分かりま す。

静的型と動的型の比較

静的型と動的型は対照的です。両者の手法の違いは一長一短なのです。静的型 言語では、実際に実行しなくても漏れなく型の不整合が見付かりますから、プ ログラムの論理エラーのうち、ある程度の割合を自動的に検出できます。

しかし、式や変数の一つひとつに型を指定する必要があるので、プログラムが 冗長になりがちですし、なんらかの継承関係があるものだけしか、ポリモーフィ ズムの対象になりません。このような仕組みは、動的言語よりも制約が厳しく 柔軟性が低いと言えます。

動的型言語はちょうど反対です。いくつかのエラーは実行してみないと分かり ませんから、プログラムの信頼性という観点で若干の不安があります。プログ ラムに型の情報がないということは、プログラムが簡潔になる半面、他人が書 いたプログラムを解釈する際にヒントが少なくなります。

しかし、とりあえず同じ名前のメソッドを持っているオブジェクトを同じよう に扱えます。つまり、型の階層について深く検討しなくてもプログラムを開発 できるということになります。生産性という面からは大変ありがたいことです *3。

多重継承の光と影 まつもと直伝 プログラミングのオキテ 第3回(3)

継承には2つの意味がある

  • 「どのようなメソッドを持っているか」あるいは「どのように振る舞うか」
  • 「どのようなデータ構造を使い、どのようなアルゴリズムで処理す るか」ということに着目した「実装の継承」

静的型言語では両者の区別が重要になります。

Javaでもこの2つを明確に区別しており、

  • 実装の継承はスーパークラスとして「extends」で指定
  • 仕様の継承の方はインタフェースと呼ばれるものを「implements」で指定
  • クラス オブジェクトの実装を指定
  • インタフェース オブジェクトの外見だけを指定

Javaでは「extends」(実装の継承)ではスーパークラスを1つだけしか指定で きません。そのため、実装の継承では単一継承となります。クラス関係が木構 造に制限できるため、クラスライブラリの構成がシンプルになります。

一方、「implements」(仕様の継承)では複数のインタフェースを指定できま す。インタフェースでは「オブジェクトをどのように扱いたいか」を指定しま す。

インタフェースにも不満がある

静的言語における多重継承の必要性を満たしながら、多重継承のデメリットで あるデータ構造の衝突やクラス階層の複雑化などを回避しています。

しかし、インタフェースが完璧な解決策かと問われると、そうは言い切れない でしょう。インタフェースに対して不満が残っているからです。ずばり「実装 を共有できないこと」が問題なのです。

多重継承の問題を回避するために、仕様の継承についてのみ多重継承を許した のですから、実装の継承が単一継承しかないことに文句を言うのはどうか、と も思いますが、一ユーザーとして不便なものはやっぱり不便です。Javaでは実 装の共有の問題への対応として、単一継承のまま、共通する機能を実装する別 クラスを作り、それを呼び出すCompositeパターンを推奨しています。

しかし、ただ単に継承の階層を越えてコードを共有したいだけなのに、わざわ ざ別の独立したオブジェクトを作り、メソッドをそのオブジェクトにいちいち 転送するのも面白くない話です。実行効率も高いとはいえません。

実装を継承する方法

動的型言語には仕様の継承という概念がそもそも存在しません。解決しなけれ ばならないのは、実装の多重継承だけです。

Lisp、Perl、Pythonでは単純に多重継承を提供しています。これで単一継承の 問題はなくなります。多重継承で起こりうる問題に関しては「気を付けて使っ てね」という立場のようです。

多重継承を変形したMix-in

RubyはJavaとも他の動的型言語とも違った手法を採っています。Rubyはモジュー ルを使ったMix-inという方法で多重継承の問題に対応しました。

Mix-inというのは元々Lisp界で始まった多重継承の使い方です。Mix-in手法に は次の2つの条件があります。

  • 通常の継承は単一継承に限る
  • 2つめ以降の継承は、Mix-inと呼ばれる抽象クラスからに限定する

Mix-inクラスは以下のような特徴を備えた抽象クラスです。

  • 単独でインスタンスを作らない
  • 通常のクラスから継承しない

これらの規則に従うことで、クラス階層は単一継承と同じ木構成になりますし、 機能の共有を実現するには、共有する機能だけを持つMix-inをクラス階層木に 「差し込む」ことで達成できます。インタフェースを使って仕様の継承問題を 解決したJavaの手法を、実装の継承に対して適用したと考えることができるで しょう。

Mix-inの実例を見てみましょう。図7[拡大表示]は、図2と図3で取り上げた SmalltalkのStreamと同等の構造を、Mix-inで構築したものです。

Mix-inを用いたクラス構成では、Streamの下に3つのサブクラスを作るだけです。 その上で、実際の入出力機能はReadable(入力)、Writable(出力)という2つ のMix-inに実装します。このMix-inをそれぞれのサブクラスに継承させること で、入力、出力、入出力という3つのクラスを実現しています。

Streamのクラス階層だけを見るとスーパークラスのStream、入出力を担当する サブクラスReadStream、WriteStream、ReadWriteStreamというように明快な木 構造になっています。クラスの構成がネットワーク状になっておらず、単純で す。さらに、共有されるコードはMix-inにまとまっているので、コードのコピー もうまく避けています。

Mix-inは、一般的な多重継承と比べてクラス構成を単純にできる優れたテクニッ クと言えます。Mix-inというルールを導入して継承を制限し、多重継承をいわ ば「飼いならす」わけです。

ちょうど構造化プログラミングが任意のgotoを制限して分岐とループを導入し たのと同じです。Mix-inは多重継承を備えた言語ならどれでも使えるので、覚 えておくと良いテクニックでしょう。

Mix-inを自由に使えるRuby

多重継承をそのまま導入している他の言語と比べ、RubyはMix-inを直接サポー トしている点に特徴があります。RubyではMix-inの単位として「モジュール」 という構造が導入されているからです。モジュールは、まさにMix-inのための 性質を備えています。

  • オブジェクトが作れない
  • 通常のクラスから継承できない

では、RubyではMix-inをどのように使うかを見てみましょう(図8[拡大表示])。 これは図7に示したStreamクラスを定義したRubyプログラムです。

Mix-inはmodule文で定義します。module文はクラスを定義するclass文とよく似 ていますが、スーパークラスを指定できません。メソッド定義などの方法はク ラスと同じです。

モジュールをクラスに取り込むためにはincludeを使います。includeを使うと そのモジュールで定義されているメソッドなどをクラスに継承します。あくま でも継承であって、コピーされるわけではないので、自クラスで同名のメソッ ドが定義されていた場合、自クラスのものが優先されます。

まとめ

単一継承と単純継承についてさまざまな面から扱いました。最後にまとめて おきましょう(表1[拡大表示])。

いやあ、今回は本当に盛り沢山でしたね。次回は表1の内容を踏まえて、静的言 語と動的言語について、特に動的言語におけるDuck Typingについて学ぼうと思 います。

Duck Typingが生まれるまで 第4回(1)

まつもと直伝 プログラミングのオキテ

型について学びます。型には静的型と動的型の2種類があります。それぞれの歴 史的な背景から始め、利点、欠点について学びます。動的型の新しいプログラ ミング・スタイルである *Duck Typing*も紹介します。

静的・動的 型

静的とは
「プログラムを実行しなくても、ソース・コードを見るだけで 分かること」という意味です。プログラムの静的な部分とは、 変数や手続きの名前と型、制御構造の構成などです。
動的とは
「プログラムを動かしてみないと分からないこと」という意味 になります。プログラムの動的な部分とは変数などの具体的な 値、実行時間、使用メモリー量などでしょう。

なぜ型が必要なのか

プログラミング言語における「型」とは、データの種別を表します。例えば 「整数」や「文字列」は型になります。ハードウエアのレベルで考えると、コ ンピュータは唯一の型、すなわち2進数だけを取り扱います。CPUを直接操作で きるアセンブリ言語では、データの型は整数値だけであって、そのほかのデー タはこの整数値の解釈によって表現されます。

  • 「静的な型」:: 変数に格納されるデータの種別(型)を指定

    プログラムを実行しなくても人間の間違いを機械的に発見できる偉大な発明

動的な型はLispから生まれた

「データそのものに自身の種別についての情報を記録する」という戦略を採る ことになりました。データ自身がデータ種別について「知っている」、このよ うなデータ型を「動的な型」と呼びます。

Duck Typingが生まれるまで 第4回(2)

オブジェクト指向で開花した動的型

動的型と静的型の巡り合い

「継承されたクラス(サブクラス)のオブジェクトを継承元のクラス(スーパー クラス)のオブジェクトと見なすことを許す」というものです。

具体的には、例えばString(文字列)というクラスがObject(オブジェクト) というクラスのサブクラスだったとすれば、Stringクラスに所属するオブジェ クトはObjectクラスのオブジェクトと見なしても差し支えない、という意味で す(図5[拡大表示])。

このルールにより、変数や式の型がコンパイル時に分かるという静的型の利点 と、実行時の型によって適切な処理へ進むポリモーフィズムが両立できるよう になったのです。

変数や式の型がコンパイル時に分かるため、型の不整合によるエラーを実行前 に発見できます。これは大きなメリットです。さらに、型情報を使ってコンパ イル時に大胆な最適化を行い、プログラムを高速化する余地も生まれました。

このような良い性質を帯びているためか、C++や1990年代になってC++の影響を 受けて生まれたJavaやC#などの静的型を持つオブジェクト指向言語が広く使わ れるようになったのです。

静的型のメリット

静的型の最大のメリットは、やはり型の不整合によってコンパイル時にバグが 発見できることでしょう。もちろん、すべてのプログラム中の間違い(バグ) をコンパイル時に発見するのは不可能ですが、バグは型の不整合を伴うことが 多いため、エラーを機械的に発見してくれる機能は、たとえ完全でなくても、 とてもありがたいことです。

一方、動的型の言語ではコンパイル時にせいぜい文法エラーしか見つけること はできません。

プログラム中で型が明示的に指定されているということは、コンパイル時に利 用できる情報が多いということです。この情報を利用してコンパイラはプログ ラムをより高速に実行できるよう工夫できます。

型情報についてはコンパイラにとどまりません。人間がプログラムを読むとき にも、「この引数の型は何か」という情報がプログラム読解の大きなヒントに なります。統合開発環境(IDE)には、この情報を利用してメソッド名などの自 動補完ができるものもあるようです。これも利用できる型情報があるために実 現できています。

最後に、変数や式それぞれが型を持つということは、変数などがどのような役 割を持つのかあらかじめきちんと考えることにつながります。プログラム記述 時に考えなければならないことは増えますが、単純に悪いこととは言えません。 むしろ、良いプログラム、信頼性の高いプログラムを書くためには必要なこと であるとも捉えられます。

こうして見ると、静的型には良いところばかりのように思えますね。しかし、 欠点あるいは課題と呼ぶべき点もいくつかあります。

その一つが、型を指定しないとプログラムを書けないことです。もちろん、型 の指定は静的型の特徴の一つです。しかし、型はあくまでも補助的な情報でプ ログラムの本質ではありません。本筋に集中したいときにいちいち型を指定す るのは煩雑ですし、一部の型宣言はただ単にコンパイラを満足させるためでは、 という気持ちになることもあります。結果的にソース・コードの分量が多くな り、本当に大事な部分が埋没してしまう可能性も否定できません。

もう一つは柔軟性の問題です。静的型があること自体、「この変数にはこの型 のオブジェクトが入る」という制限を課したことになります。このような制限 が将来の変化の足かせとなる可能性があります。前回学んだ多重継承やインタ フェースを使うと、入り組んだ継承関係が生まれます。このとき指定すべき型 を適切に選択するのはそれなりに難しい課題でしょう。

まとめると、静的型はプログラムを書く人間が型宣言という形で積極的に情報 を与えることによって、コンパイラや将来そのプログラムを読む人間が楽をし ようというアプローチであると言えます。

Duck Typingが生まれるまで 第4回(3)

動的型のメリット

では、もう一方の動的型についてはどうでしょう。動的型を採用したプログラ ミング言語の最大の利点は、ソース・コードが簡潔になることです。プログラ ミング言語はより簡潔により多くのことをコンピュータに伝えるために進化し てきました。きちんと動き、エラーも検出できるのであれば、プログラムの動 きの本質とは関係のない型指定などない方が良い、というのも一つの考え方で す。

プログラムが簡潔に記述できれば、プログラムを書くときに、型のような処理 の本質に関係ない部分のことを考えなくても済みます。本質に集中した簡潔な 記述ができれば、生産性も向上することでしょう。

一方、いくら簡潔に記述できてプログラムが書きやすくなっても、型情報がな ければ、プログラムを読解しにくくなるのではないか、という懸念もあります。 書きやすくても読みにくければ仕方がありません。これに対しては、処理の本 質に集中した簡潔なプログラムは、書きやすいだけでなく読みやすい傾向があ ると答えられます。実際、動的型の言語(例えばRuby)のプログラムと、静的 型の言語(例えばJava)のプログラムを比べると、コード量で数倍の差がある ことも珍しくありません。多くの人は動的型の言語の方が読みやすいことが多 い、と感じているようです。

簡潔な記述に対しては、プログラムの実行が遅くなるのではないかという懸念 もあります。実際、同様の処理を進めるプログラムを静的型の言語と動的型の 言語で実行すると、多くの場合、静的型の言語が勝ちます。

これに対しては、動的型に付き物の実行時の型チェックのコストも影響してい るでしょう。さらに、静的型の言語はソース・コードを直接実行できる形式に 変換するコンパイル型処理系が多いのに対して、動的型の言語はソース・コー ドを解釈しながら(一度内部表現に変換してから)実行するインタプリタ型処 理系が多いことも一因です。プログラムといっても、一般には実行性能が重要 でない局面も多く、コンピュータの性能向上により、そのような領域は増大し ているとも言えます。

動的型のもう一つのメリットは、柔軟性です。動的型の言語で書かれたプログ ラムでは変数などの型が宣言されていませんから、プログラム開発時に想定し ていなかったデータを取り扱うことが容易です。この柔軟性のカギになる概念 が以下で解説するDuck Typingです。

動的型の最大のデメリットは、実際に実行してみないとエラーを発見できない ことでしょう。静的型の言語がプログラム全体を機械的にチェックできるのに 比べると物足りなく感じます。

見かけにこだわるDuck Typing

動的型の柔軟性を表現する概念がDuck Typingです。これは西洋の格言に由来し ます。

If it walks like a duck and quacks like a duck, it must be a duck.(ア ヒルのように歩き、アヒルのように鳴くものはアヒルに違いない)

ここから、「アヒルのように振る舞うものは、その実体がなんであってもアヒ ルと見なす」というルールを引き出すことができます。あるオブジェクトがど のクラスに所属するオブジェクトかは一切考慮せず、どのように振る舞うか (どのようなメソッドを持つか)だけに関心を払うのがDuck Typingです。 Duck Typingを言い出したのは「達人プログラマ」として知られるDave Thomas です。

Duck Typingの具体例を見てみましょう。ファイルにログ・メッセージを出力す るlog_puts()という手続きがあるとしましょう。このメソッドは2つの引数 (出力先、メッセージ)を取るとします。静的型の言語(例えばC++)であれば 次のようなコードになるでしょう。

void log_puts(ostream out, char* msg);

log_puts()手続きは、出力先outに時刻とメッセージを出力します。次のよう に呼び出す例を取り上げます。

log_puts(cout, "message");

例えば、以下のようなログをcout(C++の標準出力)に書き込むことになるでしょ う。

2005-06-16 16:23:53 message

さて、ここでログの出力先をファイルではなく、文字列にしたくなったらどう しましょうか。

出力先を指定する引数outはostreamであると決まっているので、簡単には変更 できません。結局、log_puts()手続き全体をコピーして文字列を出力対象に する別の手続きを用意するか、一時ファイルに出力しておいて、文字列に読み 込み直すかしかありません。

では、Duck Typingを使うとどのように柔軟なコードになるのでしょうか。 Duck Typingを使うと、次のようになります。

log_puts(out, msg)

動的型ですからプログラム上で型は指定していません。C++の例と同様に以下の ような呼び出しはSTDOUT(Rubyにおける標準出力)に同じようなログを書き込 みます。

log_puts(STDOUT, "message")

さて、先ほどと同じように文字列に対して出力したくなったとしましょう。 Duck Typingでは話がずっと簡単になります。「出力先(標準出力)と同じよう に振る舞うものであれば、それを出力先に使ってもよい」のです。

Rubyには文字列に対してファイルと同じように入出力を行うStringIOというク ラスが用意されています。StringIOを使って入出力を記述した例を図6[拡大表 示]に示します。

StringIOクラスとSTDOUTのクラス(IO)の間には継承関係がありません。しか し、StringIOクラスはIOクラスの持つすべてのメソッドを備えています。です から、StringIOクラスはほとんどの局面でIOと同じように使うことができます。

静的型の言語で同じことをしようと思えば、logを出力するのに必要な「振る舞 い」を用意したクラス(Javaの場合、インタフェース)を用意してlog_puts() の最初の引数の型に指定する必要があります。今回の例のように、この型が組 み込みの型であったら、「出力先」を表現する別のオブジェクトを新たに作る 必要があるでしょう。このような仕組みを最初から用意するのは大変ですし、 途中から導入するとなれば、プログラムのあちこちに大規模な改修が必要にな るでしょう。

静的型は、プログラム開発者が型宣言としてたくさんの情報を提供するために、 エラーの検出が早く、確実に実行できます。そのかわり、型を設計した時点の 前提が変化すると、指定したたくさんの情報(型)を一貫性を保ちながらすべ て更新しなければなりません。動的型は、最初からそのような指定を行ってい ませんから、変化に強い傾向があります。

では、動的型の言語でDuck Typingを実践するにはどのような指針に従えばよい のでしょうか。基本的な原則はたった1つ、最低限これだけを覚えておけば大丈 夫です。

●明示的な型チェックを避ける

プログラム中で、引数の型をチェックしたくなる場合もあります。例えば、図 7[拡大表示]のように、文字列を期待している部分があったとすると、Stringク ラスのオブジェクトでなければ例外を発生させてエラーにしたい、という気持 ちが自然に生まれてきます。

しかし、Duck Typingを実践するときには、ここでぐっと我慢する必要がありま す。クラスを基準としたチェックを行えば、静的型と同様に柔軟性を失ってし まうからです。どうしてもチェックしたい場合でも、「あるクラスのオブジェ クトか」ではなく「あるメソッドを持っているか」という条件でチェックして ください(図8[拡大表示])。

しかしながら、そもそもチェックを行わなくても、期待と違うオブジェクトな らば「メソッドが見付からない」というエラーが発生するはずです。

動的型のデメリットを克服

動的型のデメリットとは大きくまとめると「エラー発見が実行時」、「読解す るときのヒントが少ない」、「遅い」の3つになります。

最初の「エラー発見が実行時」という点は単体テストをきちんとこなすことで 克服できます。きちんとテストを進める習慣が定着していれば、コンパイル時 の型チェックがなくても信頼性が下がることはありません。

「プログラムの読解のためのヒントが少ない」点はドキュメントの整備が解決 になるでしょう。JavaにはJavaDoc、RubyにはRDocというソース・コード中にド キュメントを同時に記述することでドキュメント維持の負担を減らす技術が存 在します。

最後に、「実行速度が遅い」点ですが、コンピュータの性能がこれだけ向上し ている昨今、ほとんどのケースで実行効率よりも柔軟性や生産性の方が重要で す。

動的プログラミング言語

現代では、プログラム開発に期待される生産性はますます高くなっています。 つまり、今までよりも多くの機能を、今までよりも短い時間で開発することを 求められます。

そのような短期間の開発に対応するため、プログラムを開発しながら、最適な 解を模索するような手法が広まりつつあります。これを「shooting a moving target」と呼ぶことがあります。これまでのように、最初にあらゆる状況を考 慮して、仕様を決定してから開発に取り掛かるというやり方では対応しきれな くなってきています。手早く開発できること、変化に素早く対応できることが なによりも求められます。

このような「俊敏さ」を求められる開発では、Duck Typingに代表されるような 実行時の柔軟性が非常に役立ちます。Ruby、Python、Perl、PHPなど動的型を持 ち、実行時の柔軟性に優れた動的プログラミング言語が注目されているのはこ のような理由があるのです。 ## メタプログラミング

メタプログラミング

概要

今回は「プログラミングをプログラムする」メタプログラミングについて学び ます。メタプログラミングを利用すると、動的にメソッドを追加するなど、実 際のアプリケーション作成に役立つ処理が簡単に実現できます。メタプログラ ミングと小言語の関係についても解説を加えました。

今回はメタプログラミングを扱います。メタとはギリシャ語で「間に、後に、越える」などを意味する接頭辞「meta」に由来する言葉で、「超越」、「高階」などの意味があります。例えば、Rubyをはじめとする多くのオブジェクト指向プログラミング言語では、「クラスのクラス」のことを「メタクラス」と呼びますし、他のオブジェクトを支えるクラス・オブジェクトなどのことをメタオブジェクトと呼ぶこともあります。

メタプログラミングとは、プログラミングをプログラミングすることです。そ んなことが何の役に立つのかと感じる方もいらっしゃるでしょう。今回は一見 して何の役に立つのか分からないメタプログラミングの能力の一端を紹介しま す。

メタプログラミング

 では、早速Rubyを用いてメタプログラミングの実例を見てみましょう。まずは動的にメソッドを生成する例です。

 Rubyのクラスに組み込まれた機能であるattr_accessorは、インスタンス変数にアクセスするメソッドを作り出します(図1)。図1に示した短いコードだけで、Personというクラスに対してnameメソッド、ageメソッドが生成されます。これらを使って代入もできます。

class Person attr_accessor :name, :age end

図1●メタプログラミングの例 Rubyではattr_accessorを用いてインスタンス変数をアクセスするメソッド(ここでは、nameとage)を生成できる。

 ここで重要なことは、このattr_accessorはこのような機能を実現する文法ではなく、Moduleクラスが提供する単なるメソッドであり、その気になれば同じような働きをするメソッドを自分で定義できる、ということです。

 attr_accessorは内部的に次のような処理を行っています。

(1)引数のシンボルすべてに対して以下の処理を繰り返す。

(2)シンボルで指定された名前のメソッドを定義する。そのメソッドはシンボルの名前の先頭に「@」を付加したインスタンス変数の値を取り出す。

(3)シンボルで指定されたメソッド名の後ろに「=」を付加した名前のメソッドを定義する。そのメソッドは引数を1つ取り、その値をシンボルの名前の先頭に「@」を付加したインスタンス変数に設定する。

 簡単な手順のようですが、CやC++のようなプログラミング言語ではなかなかできないことです。なぜなら、C++などではプログラムの実行中、簡単には動的にクラスにメソッドを追加できないからです。Rubyでは図1のように簡単に実現できています。実際にはattr_accessorはCで実装されていますが、同じ処理をRuby自体で実現するならば図2のようになるでしょう。

class Module

 def attr_accessor(*syms)
  syms.each do |sym|
  class_eval %{
   def #{sym}
    @#{sym}
   end
   def #{sym}=(val)
    @#{sym}=val
   end
  }
  end
 end
end

図2●attr_accessorをRuby自体で実装した例

 class_evalは文字列を受け取って、それをクラスの文脈で評価するメソッドです。図2では「%{」から対応する「}」までの間が文字列としてclass_evalに渡されています。文字列の中では「#{」と「}」で囲むことで式を埋め込むことができますから、変数symで示されるメソッド名が埋め込まれた文字列が評価され、ループの繰り返しごとに2つのメソッドが定義されることになります。ですから、次のような呼び出しがあると、

attr_accessor :name

呼び出しの対象となったクラスにnameという名前でインスタンス変数@nameの値を取り出すメソッドを定義し、name=という名前でインスタンス変数@nameの値を設定するメソッドを定義します。

 メタプログラミング機能を持たない言語では、このような処理を実現しようとするとかなり手間がかかります。言語の文法そのものを拡張するか、マクロのようなプログラムを前処理するプリプロセッサを導入するかしかありません。いずれにしても通常の言語にとっては大げさなことになってしまいます。

メタプログラミング リフレクション

リフレクション

メタプログラミングの例として次にリフレクション(reflection)を取り上げましょう。英単語では「反射」とか「反省」を意味しますが、プログラミングの文脈で用いられた場合、実行中のプログラムの情報を取り出したり、変更したりする機能のことを指します。

Rubyの場合、表1に挙げたリフレクション機能を備えています。変数やメソッドの一覧を得たり、値を取得・変更したりするさまざまな手法が用意されていることが分かります。考えてみれば、2005年7月号で解説したMix-inを行うincludeですら、Rubyの文法ではなく、メソッドによって実現されています。Rubyでは動的にプログラムを操作するという性質が徹底されているのです。

表1●Rubyの備えるリフレクション機能

では、これらの機能を使うとどのようなことができるのか、具体的な事例を見ながら考えてみましょう。

メタプログラミングの事例

 まずはリフレクションを用いた事例を紹介します。

 あるオブジェクトに対する呼び出しを別のオブジェクトに転送したい場合があるでしょう。Rubyでは委譲を行うためのライブラリとしてDelegetorが用意されています。Delegatorオブジェクトはメソッドの委譲先のオブジェクトを持ち、メソッド呼び出しを委譲先に転送します。デザイン・パターンで言うとProxyパターンの基礎部分を実現するものです。Delegatorを使うためには、SimpleDelegatorクラスを使います。

require 'delegator'
d = SimpleDelegator.new(a)

 これだけでオブジェクトdに対するメソッド呼び出しはすべてオブジェクトaに転送されます。転送されるだけではあまりうれしくないのですが、このオブジェクトに特異メソッド*1を付加して一部だけ挙動を変えるなど、さまざまな使い道があります。

 以上の処理を他の言語、例えばJavaで実現するとしたらどうなるでしょうか。静的型を持つJavaでは型を合わせるため、aの型に対応してメソッドを転送する専用のDelegator用クラスを個別に用意する必要があります。aのクラスにいくつメソッドがあるかは分かりませんが、場合によっては非常に数が多くなることも考えられます。いずれにしても、おそらくは専用のツールで自動生成でもしないことには現実的ではないでしょう。

 DelegatorはRubyの動的型をリフレクションの組み合せによって初めて実現できているといえるでしょう。

リフレクション機能を使う

それでは、Delegatorクラスを実現するためにRubyのリフレクション機能がどのように使われているかを見てみましょう。図3はSimpleDelegatorの実装の一部です。理解のために実際のものよりもかなり単純化してあります。

class SimpleDelegator
 # (a)メソッドの未定義化
 preserved = ["__id__", "object_id", "__send__", "respond_to?"]
 instance_methods.each do |m|
 next if preserved.include?(m)
 undef_method m
 end

 # (b)オブジェクトの初期化
 def initialize(obj)
  @_sd_obj = obj
 end

 # (c)method_missing
 def method_missing(m, *args)
   unless @_sd_obj.respond_to?(m)
   super(m, *args)
  end
  @_sd_obj.__send__(m, *args)
 end

 # (d)メソッド・チェック
 def respond_to?(m)
  return true if super
  return @_sd_obj.respond_to?(m)
 end
end

図3●SimpleDelegatorの実装内容

 図3のコードは、大きく4つの部分に分かれています。まず、一番重要なところから説明しましょう。(c)で示した部分がDelegatorの心臓部です。Rubyはメソッド呼び出しを行った際に、オブジェクトがそのメソッドを知らない場合、まずmethod_missingという名前のメソッドを呼び出します。method_missingの第1引数は呼び出されていたメソッド名、残りがそのメソッドに渡されるはずだった引数です。method_missingのデフォルトの実装は例外を発生させますが、これをオーバーライドすることで未知のメソッドに対応させています。ここでは、次の2つの処理を進めています。

(1)委譲先のオブジェクトがそのメソッドを知らなかったら(respond_to?)、デフォルトの実装を呼び出し(super)、エラーを発生させる。

(2)そうでなければ、__send__を使って委譲先のメソッドを呼び出す。

 __send__というのはオブジェクトのメソッドを呼び出すメソッドです。このメソッドにはsendという別名がありますが、ありふれた名前でいかにも重複しそうなので__send__という名前の方を採用しています。

 SimpleDelegatorの残りの部分の実装は比較的簡単です。説明した通り、SimpleDelegatorはmethod_missingを使ってメソッドの転送を行っています。しかし、RubyのObjectクラスは比較的たくさんのメソッドを提供する「大きなクラス」です。Objectクラスは実に40ものメソッドを抱えています。SimpleDelegatorはObjectクラスのサブクラスですが、これら40もの「知っているメソッド」を転送できないのは困ります。そこで、(a)の部分で、instance_methodsで得られるメソッドの一覧を使って、必須のメソッド(__id__、object_id、__send__、respond_to?)を除いたメソッドを未定義化しています。

 (b)ではSimpleDelegatorの初期化の部分で委譲先を設定しています。(d)の部分ではrespond_to?が正しく反応するように、まずsuperを使って自分のメソッドを調べた後、委譲先のメソッドをチェックするようにしています。

メタプログラミング 分散Rubyを実現する

まつもと直伝 プログラミングのオキテ 第6回

分散Rubyを実現する

 「呼び出されたメソッドをそのまま他のオブジェクトに転送する」Delegatorの機能はほかにもいろいろと応用できそうです。一例としてdRuby(Distributed Ruby、分散Ruby)を紹介します。

 dRubyはネットワーク経由でメソッドを呼び出すライブラリです。dRubyはサーバー上に存在するリモート・オブジェクトに対応するオブジェクト(Proxy)を作り出し、そのProxyへのメソッド呼び出しをネットワークを越えて転送します。

 呼び出されたメソッドはサーバー上のリモート・オブジェクトで実行され、戻り値が再びネットワークを経由して返ってきます。JavaでいうRMI(Remote Method Invocation)などと似たような仕組みですが、Rubyのメタプログラミング機能を利用して、明示的なインタフェースを定義することなく、任意のオブジェクトのメソッドをネットワーク経由で呼び出せます。

 C++やJavaでのリモート・メソッド呼び出しではIDL(Interface Definition Language)のような言語によってインタフェースを記述し、自動生成されるスタブをコンパイル・リンクしなくてはいけません。これと比較するとなんと簡単に扱うことができるのでしょう。これこそがメタプログラミングの力です。

 dRubyの最初の版は、わずか200行で実装されたのだそうです。これもメタプログラミングの力だと思います。ただし、現在ではdRubyはRubyに標準添付されており、総行数では2000行を越える大規模ライブラリに成長しています。

データベースに応用する

 データベース分野でもメタプログラミングが使われています。

 Webアプリケーション・フレームワークRuby on Rails(RailsまたはRoRと呼ばれる)*2にもメタプログラミングが隠れています(関連記事「生産性の高いWeb開発環境 Ruby on Rails」を参照)。具体的にはデータベース連携ライブラリ(ActiveRecord)が、メタプログラミング機能を使い、簡単にデータベース・レコードと対応するオブジェクトを定義しています。

 図4はActiveRecordを使ったデータベースの定義の例です。これとは別にデータベースにテーブルが定義されているとします。Userクラスに対応するもの(usersテーブル)だけを図5に示しました。

class User < ActiveRecord::Base
 has_one :profile
 has_many :item
end

class Profile < ActiveRecord::Base
 belongs_to :user
end

class Item < ActiveRecord::Base
 belongs_to :user
end

図4●ActiveRecordによるレコード定義の例

CREATE TABLE `users` (

 `id` int(11) NOT NULL auto_increment,
 `login` varchar(80) default NULL,
 `password` varchar(40) default NULL,
 PRIMARY KEY (`id`)

) TYPE=MyISAM;

図5●図4で用いるusersテーブル定義の内容

 図4に示したわずかなコードから、ActiveRecordは、次のような処理を進めます。

(1)クラス(User)とクラス名を複数形にしたテーブル(users)を連携させる。

(2)テーブルのスキーマからレコード内容にアクセスする手段を用意する。

(3)has_one、belongs_toなどの関連指定から、関連するオブジェクトを取り出す手段を用意する。

 このような処理ができるのも、メタプログラミング機能によってクラス名を取り出したり、メソッドを実行時に追加したりできるおかげです。メタプログラミングを応用することで、Railsは生産性の高いWebアプリケーション・フレームワークだという評判が高まっています。

 もちろんRailsが万能で、他のあらゆるフレームワークよりも優れているというわけではありません。しかし、Rubyという言語の特徴を最大限に活用して、非常に高い生産性を確立しているのは確かです。

 Railsについての最新情報は、Webサイトから入手できます。このサイトでは「15分で分かるRails」といった雰囲気で実際にWebアプリケーションを作るビデオを視聴できます。目の前であっという間にWebアプリケーションを作ってしまう様子には感銘を受けます。これは必見です。英語ですが、雰囲気は伝わってきます。

XMLを出力する

 最後に、XMLファイルを出力するためのライブラリを紹介しましょう。Jim Weirich氏が開発したXmlMarkupです。図6のようなプログラムでXMLを簡単に出力(図7)できます。

require 'builder/xmlmarkup'

xm = Builder::XmlMarkup.new(:indent => 2)
puts xm.html {

 xm.head {
  xm.title("History")
 }
 xm.body {
  xm.h1("Header")
   xm.p {
    xm.text!("paragraph with ")
    xm.a("a Link", "href"=>"http://onestepback.org")}
 }
}

図6●XmlMarkupのプログラム例と出力内容

<html>
 <head>
  <title>History</title>
 </head>
 <body>
  <h1>Header</h1>
  <p>
  paragraph with <a href="http://onestepback.org">a Link</a>
  </p>
 </body>
</html>

図7●図6の出力内容

Builder::XmlMarkupはDelegatorと同様にmethod_missingを用いるテクニックを使っています。メソッド呼び出しによってタグ付きのXMLを出力します。

タグの付いていないテキストにわざわざtext!を付けなければなりませんが、XMLという人間が書くには手間がかかるフォーマットを、Rubyのブロックを使ってうまく表現しています。

メタプログラミング メタプログラミングと小言語

メタプログラミングと小言語

ここまで紹介してきたようなメタプログラミング機能をあなたならどのように使いますか。

Glenn Vanderburg氏*3によれば、メタプログラミング機能が最も活用できるのはDSL(Domain Specific Language)の分野なのだそうです。DSLとは特定の分野向けに機能を強化した小規模なプログラミング言語のことで、昔からあるアイディアです。ユーザーがアプリケーションを強化したり、カスタマイズしたりするために活用されるなど、最近再び注目されている考え方です。同氏によればDSLは問題分野に特化するため、表2のような機能を備えていることが望ましいのだそうです。

表2●小言語が備えるべき機能

このうち、「型」から「制御構造」まではRubyに元々備わっています。多くのDSLはミニ言語でこの辺り手を抜いていることが多いので、かえって使いやすいかもしれません。また、前回学んだようにRubyはブロックを使って制御構造を実現するメソッドを自分で定義できるというのも利点です。

残りの「宣言」から「階層データ」まではRuby自身に備わった機能で実現できます。この実現にはRubyのメタプログラミング機能が活躍するのです。

Rubyの宣言

表2にある「宣言」について考えてみましょう。

今回、冒頭でattr_accessorをメタプログラミングの例として紹介しました。Rubyとして見たときには単なるメソッド呼び出しですが、宣言として考えることもできます。ActiveRecordの例でもhas_manyなど宣言として考えることができるメソッドの例はたくさんあります。

Rubyではメソッドがプログラム自身の状態を読み出したり、変更したりできるので、通常のメソッド呼び出しで、他の言語であれば「宣言」によって進めるような内容を実現できます。

外見の点からは、Rubyのメソッド呼び出しはかっこの省略ができる点、また「名前を表現するもの」として:fooのような「シンボル」が使える点でより宣言らしいプログラムの見かけが実現できています。

Rubyの文脈依存

次は「文脈依存」です。文脈依存とは文脈によってある一定の範囲だけ語彙(ごい)をすりかえることです。少々人工的な例ですが、図8のようなプログラムを考えます。

add_user {

 name "Charles"
 password "hello123"
 privilege normal

}

図8●文脈依存のプログラムの例

この例ではadd_userで指定したブロックの範囲内でだけ、nameやpasswordなどのメソッドが有効になっています。つまりadd_userのブロックの外側ではこれらのメソッドは見えないわけです。

Rubyレベルではこれはブロックの範囲内だけメソッドの受け取り手であるselfをすりかえることで実現しています。具体的には図9のようにinstance_evalメソッドを使います。

def add_user(&block)

 u = User.new
 # User class has name, password,
 # privilege methods
 u.instance_eval(&bock) if block

end

図9●図8でコンテキストをすりかえている処理

instance_evalメソッドがブロックを受け取ると、selfを置き換えた状態でブロックを実行します。結果として、図8の例ではブロックの範囲内でデフォルトのレシーバがUserクラスのインスタンスuになります。そのため、レシーバを指定せずに実行するメソッド(nameなど)としては、Userクラスのメソッドが呼び出されます。

Rubyの単位

一般的なプログラミング言語では値として「スカラー値」を扱います。これは数そのものです。その数が表現している単位はプログラマ側で管理する必要があります。

一方、DSLで扱いたいのは単なる数ではなく「量」であることが多いのです。そのため、いくつかのDSL的アプローチに則ったライブラリでは「単位」を取り扱うように拡張されています。

例えばRuby on RailsではNumericクラスとTimeクラスに時間を扱うための単位メソッドが追加されています(基本単位は秒)。例えばある時間を表現するためには

3.years + 13.days + 2.hours

と書きます。するとこれは「3年と13日と2時間」を秒で表現した整数95803200 となります。また、次のようにして「今から4カ月後の月曜日」を表す時刻を得 ることができます。

4.months.from_now.monday

原稿執筆時に計算した結果は以下のようなものでした。

Mon Dec 12 00:00:00 JST 2005

これは時刻と時間に関する例ですが、既存のクラスにメソッドを自由に追加できるRubyでは、このように単位を表現するメソッドを簡単に実現できます。

Rubyの語彙

DSLが目的分野に特化しているというのは、結局、どれだけその目的分野で行われる処理を表現する語彙を持っているかということでしょう。ある分野で必要としているクラスやメソッド、手続きをRubyで定義するとは、Rubyをその分野向けの専用言語化することだと考えられます。そのようなクラスやメソッドのことをその分野における「語彙」と呼んでも良いでしょう。

「達人プログラマ」として知られるDave Thomas氏の言葉を借りれば「すべてアプリケーションを作る過程は結局言語をデザインすることである」のだそうです。その見方に従えばアプリケーションを作ることは、そのアプリケーションの問題領域の語彙をどんどん定義していき、最後にその語彙を使って問題解決手段を記述することにほかなりません。

Rubyのメソッド呼び出しやブロックなどの表現力を使うと、ユーザーにとってより自然な形で語彙を定義できます。また、語彙があらかじめ決定できない場合には、DelegatorやXmlMarkupのようにmethod_missingという手法を使って動的に語彙を追加・利用できます。

Rubyの階層データ

最後に表2の末尾にある「階層データ」を説明します。先ほど紹介したXmlMarkupはまさに階層データの表現になっています。図6を再び眺めてみましょう。プログラムとして見たときには単なるブロック付きのメソッド呼び出しがネストしているだけのことですが、外見上も機能上も立派な階層データの表現です。

メタプログラミング 言語内DSLに向く言語、向かない言語

言語内DSLに向く言語、向かない言語

まとめてみると、Rubyは非常にDSL向きな言語だと分かります。

まず、メソッド呼び出しにかっこを省略できることなど表現の多様性により、プログラムを宣言っぽく見せかけることができるので、宣言的な表記が可能になります。DSLで必要とされる機能の多くはデータ構造の表現や、設定など宣言的に表記されることが多く、宣言としての外見を提供できることは重要です。

さらに、メタプログラミング機能によって、プログラムの情報を取得・更新できるため、DSLに必要な機能をプリプロセッサやマクロを使うことなく実現できます。このように言語を拡張することなくDSLを実現するアプローチを「言語内DSL」と呼んでいます。

言語内DSLに向いた言語はRubyばかりではありません。Rubyが大きく影響を受けたLispやSmalltalkはRubyと同じように言語内DSLに適しているとされています。特にLispは固定的な文法が原則的にS式という構造データ表現しかありませんから、ほぼ任意の言語内言語を構築可能です。

Rubyならevalで文字列処理によってプログラムを構築するような局面でも、Lispならマクロによるリスト処理でプログラムを処理できます。Lispのことを「Programmable Programming Language」と呼ぶ人もいるほどです。

SmalltalkはLispほど極端ではありませんが、Rubyに負けないくらい動的で、かつメタプログラミング機能を持っています。Smalltalkでは元々制御構造もブロックを使って表現しているくらいですから、文法の拡張も思いのままです。

一方、他の手法を使わないと言語内DSLが実現しにくい言語もあります。例えばC++、Java、C#などの言語ではRuby、Lisp、Smalltalkと同じようなやり方ではDSLを実現できないでしょう。

しかし、このような言語でもDSLというアプローチを利用できないわけではありません。一つはコード生成です。これはDSLのための「ミニ言語」を用意して、それをC++、Java、C#などのターゲット言語に「コンパイル」するものです。このコンパイルにはしばしばRubyのようなテキスト処理に優れた言語が用いられます。「Code Generation inAction」*4という書籍ではまるまる1冊このテーマが解説されています。

もう一つのDSLの実現方法はインタプリタを用意するものです。とはいえ、毎回アプリケーションごとに言語を文法から設計・実装するのも大変なので、定型の文法を採用して、ライブラリ・ルーチンを使って読み込みます。

具体的には文法にXMLを使って、DOM(Document Object Model)などのXML処理ライブラリを使って文法解釈を行います。Javaアプリケーションの設定ファイルにXMLが採用される理由の一つがこれです。XMLファイルによって、いちいちJavaプログラムをコンパイルすることなくアプリケーションの設定を変更したり、挙動をカスタマイズしたりできるようになります。

このような使われ方をしている場合、XMLはJava界のDSL、あるいはJavaアプリケーションのスクリプト言語と考えることができるでしょう。

今回はRubyを軸にメタプログラミング機能とその応用、特にDSLについて解説しました。Rubyの機能の応用範囲の広さについて感じていただけたのではないでしょうか。

著者: suzuki@cis.iwate-u.ac.jp

Created: 2015-10-30 金 21:01

Emacs 24.5.1 (Org mode 8.2.10)

Validate