Ruby は動的な言語である。それを知っており、また高く評価している者にとってこれは繰り返して述べるまでもないことだ。おそらくそのような人々は動的な言語が静的な言語より何らかの意味において優れていると信じている。
実際のところそれは何を意味しているのだろうか? どういう理由で、開発時でなく実行時に事をなさねばならないのか? いやそもそもそんなことを気にする必要がどこにあるのか?
1990年に初めて動的言語という概念に出会ったとき、私にもピンとこなかった。ある友人が Smalltalk という言語について興奮した口ぶりで、「クラスやメソッドを実行時に追加することができるのよ」などと教えてくれた。これには私もそれなりに感心した。しかし一方で「それがどうして必要なんだ? 車を停めずにタイヤを交換しようとするようなものじゃないか。」とも思った。
さて、ここにあるプログラミング課題を用意した。データの形式に応じてコードを書き変えないといけないような課題だ。ごく単純な CSV (conmma-separated values) ファイルを用いることにしよう。一行目は説明のためのヘッダになっている。コードが理解しやすいものになるよう、他にもいろいろ単純化しながら話を進めていく。
また下のような内容のデータファイル people.txt を用いる(データ中の氏名はすべて架空のものだ)。
file: people.txt
----------------
name,age,weight,height
"Smith, John", 35, 175, "5'10"
"Ford, Anne", 49, 142, "5'4"
"Taylor, Burt", 55, 173, "5'10"
"Zubrin, Candace", 23, 133, "5'6"
静的な言語ならば、データの形式は開発の時点でコードに反映される。あるいは全く反映されない。例として C や C++ で仕事を進める場合を考えてみよう。おそらく構造体(C の場合)やクラス(C++ の場合)を定義して、それらに name や age といったメンバ変数を持たせることになる。しかし、このやり方は個別的に過ぎる。データの要素名がすべてハード・コーディングされてしまうからだ。また、新しいフィールドが追加されるなどしてファイル形式が変わるたびにコードに変更を加えねばならず、いささか信頼性に劣る。
それならばということで、データの個別的性質から独立したソリューションをとることもできる。要素名と値のペアをハッシュや連想配列に格納するのだ。考えとしては悪くない。しかし、この方法ではデータの各要素をいわば「二級市民」に格下げしてしまう。新たな処理のレベルを設ないとそれらを取得できないからだ。name や age といった要素は単なる文字列であり、属性名として直接扱うことができない。
一つ目のソリューションでは、データを自然な形で扱えるが融通が利かない。二つ目のソリューションは強力かつ汎用性に富むが、データの扱いが自然でない。両者の利点だけを享受できる選択肢はないものだろうか?
実はそれが存在する。Ruby のような動的言語では、メタプログラミングを行うことで、すなわち「コードを作成するコードを書く」ことで、実行時であっても外部データを「一級市民」として扱うことができる。
どうやったらそれができるのか? 今からゆっくり見ていくことにしよう。
まず、クラスを作成する際、クラス名はファイル名から取得できると想定する。またファイルの1行目には属性名のリストが与えられていると想定する。(簡略化のため、今回はエラーチェックをほとんど行わない。例えば、属性名が Ruby コードにおいて妥当なメソッド名となるかどうか、形式の異なっていたり欠損していたりするデータが無いかどうかなどをチェックしない。)
ファイル名からクラス名を得たならば、次は内容の取得だ。ファイルはデータ要素のリストになっていると想定し、それらをオブジェクトの配列に順次読み込んでいく。
では最初に新しいクラスを作成し、適切な名前を与えるところから見ていこう。
class_name = File.basename(file_name,".txt").capitalize
# 例 "foo.txt" => "Foo"
klass = Object.const_set(class_name,Class.new)
Class.new というコードは新しいクラス(この時点ではまだ無名クラス)を作り出す。const_set というのは値を定数名に結びつけるメソッドだ。(Ruby のすべてのクラスは、さらに言えばトップレベルにおけるすべての定数は、Objectの 一部であり、したがって、例えば Fixnum は Object::Fixnum と同義である。)
この新しいクラスは変数 klass に代入される。ファイル名が people.txt であれば、このクラスは People というクラス名になるはずだ。
では次にクラスに属性を加えていこう。データの1行目は属性名のリストであった。コンマ記号によって文字列の配列に分解していく。
data = File.new(file_name)
names = data.gets.chomp.split(",") # 文字列の配列
ここまで来れば新しいクラス klass のコンテクストで class_eval メソッドを呼ぶことができる。その際、初期化用メソッドも定義しておく。
klass.class_eval do
attr_accessor *names
define_method(:initialize) do |*values|
names.each_with_index do |name,i|
instance_variable_set("@"+name, values[i])
end
end
# その他いろいろな処理
end
これにより、新しいクラスに対して一揃いのアクセサ(値の読み書きのためのメソッド)が定義される。また initialize メソッドにより、new を呼べば決まった順でインスタンス変数に値が代入されるようになる。
ここで変数 names がブロックの外で最初に用いられていることに注意してほしい。Ruby においてブロックはクロージャである。そのため通常ならスコープから外れ、ガベージコレクションの対象となるような時点においても、ブロックからは names が参照可能である。
上のコードの最後に「その他いろいろな処理」とある。実用的なクラスを作成するのに何かやっておくべきことはないだろうか? 気の利いた to_s メソッドを定義し、puts が使えるようにしておくと良いだろう。さらに利便性を考えて、inspect という alias も与えておこう。
# class_eval の内部
define_method(:to_s) do
str = "<#{self.class}:"
names.each {|name| str << " #{name}=#{self.send(name)}" }
str + ">"
end
alias_method :inspect, :to_s
他には何かあるだろうか? ファイル全体を読み込んでオブジェクトの配列を返すクラスメソッドを作っておこう。クラスメソッドなので class_eval は必要ない。クラスを保持している klass オブジェクトに特異メソッドを追加すればいい。
def klass.read
array = []
data = File.new(self.to_s.downcase+".txt")
data.gets # ヘッダ部分を読み飛ばす
data.each do |line|
line.chomp!
values = eval("[#{line}]")
array << self.new(*values)
end
data.close
array
end
このメソッドはまずオブジェクト自身の名前をもとにデータファイル名を決定する(例えば People というオブジェクトは people.txt というファイル名にマッピングされる)。次にファイルの1行目を読み飛ばす。最初に同じファイルを読み込んだときはこの1行目をクラス作成に必要な情報として用いたが、今回はクラスがすでに出来上がっている。現時点でインスタンスはまだ生成されていないが、ひとたび read を呼び出せば、クラスのインスタンスから成る配列が返ってくる。
再利用性を考えて、ここまでの全体を一つのクラスにまとめ、my-csv.rb という(工夫のかけらもないような)名前のファイルに保存しよう。そして次のようなメソッドを定義する。この make メソッドは引数としてファイル名をとり、そのファイルの内容にもとづきクラスを作成する。
# ファイル: my-csv.rb
class DataRecord
def self.make(file_name)
# ここまでの全コード
klass
end
end
クラス定義中の self はこのクラス自体を指す。したがって def self.make という行を、def DataRecord.make と書き換えてもプログラムの振る舞いは変わらない。しかし self を用いるほうが、クラス名を DataRecord 以外のものにしたときにもコード変更の必要がないという点で少しばかりベターである。
上のコードの最下部に klass とだけ書かれた行があるが、これは何を意味するのだろうか? 実はこの部分で呼び出し元に値を返している。これによって、例えば People という名のクラスを作ったときでも、「固有名」にしばられることなく、好きな変数に代入して使うことができる。
もちろん、klass の代わりに return klass と書くこともできる。いずれにせよ Ruby では最後に評価された式がメソッドの戻り値になる。
それではこの小さな my-csv.rb ライブラリの全コードを下に示す。
# ファイル: my-csv.rb
class DataRecord
def self.make(file_name)
data = File.new(file_name)
header = data.gets.chomp
data.close
class_name = File.basename(file_name,".txt").capitalize
# "foo.txt" => "Foo"
klass = Object.const_set(class_name,Class.new)
names = header.split(",")
klass.class_eval do
attr_accessor *names
define_method(:initialize) do |*values|
names.each_with_index do |name,i|
instance_variable_set("@"+name, values[i])
end
end
define_method(:to_s) do
str = "<#{self.class}:"
names.each {|name| str << " #{name}=#{self.send(name)}" }
str + ">"
end
alias_method :inspect, :to_s
end
def klass.read
array = []
data = File.new(self.to_s.downcase+".txt")
data.gets # ヘッダ部分を読み飛ばす
data.each do |line|
line.chomp!
values = eval("[#{line}]")
array << self.new(*values)
end
data.close
array
end
klass
end
end
次に、これを利用した小さなプログラムを作ってみよう。例の people.txt ファイルを読み込んで、オブジェクトの配列を作って返す。そしてファイルの最初の要素をプリントアウトする。
require 'my-csv'
data = DataRecord.make("people.txt") # 戻り値であるクラスを代入し
list = data.read # クラスメソッドを呼び出す
puts list[0]
# アウトプット:
# <People: name=Smith, John age=35 weight=175 height=5'10>
ここでは、作成されたクラスが make メソッドの呼び出し元に戻り値として与えられることを利用している。なお、この新しいクラスにはそれ自体の名前が与えられているため、直接アクセスすることも可能だ。次のコードは上のコードと実質同じである。
require 'my-csv'
DataRecord.make("people.txt") # 戻り値は無視する
list = People.read # クラスをクラス名を用いて参照する
puts list[0]
# アウトプット:
# <People: name=Smith, John age=35 weight=175 height=5'10>
さて、上のやり方が上手くいくことは分かった。しかし、他のやり方と比べてどういう点で優れているのだろうか。
第一に、今回作成したプログラムでは属性が「一級市民」として扱われている。ハッシュや参照テーブル内の単なる文字列としてではない。このことで得られる利便性は計り知れない。
person = list[0]
puts person.name # Smith, John
if person.age < 18
puts "under 18"
else
puts "over 18" # over 18
end
kg = person.weight / 2.2 # kilograms
第二に、すでに述べたとおり、動的処理のテクニックを用いている。全く新たなデータファイルを利用して、この点を分かりやすく示そう。(数値は完全にでたらめだ。)
ファイル: places.txt
----------------
latitude,longitude,description
47.23,59.34,Omaha
32.17,39.24,New York City
73.11,48.91,Carlsbad Caverns
先ほど作ったプログラムのうち、クラス名を使わないバージョンのほうを実行してみよう。そのままで問題なく完了し、予想通りのアウトプットが得られるはずだ。
# アウトプット:
# <Places: latitude=47.23, longitude=59.34, description=Omaha>
鋭い読者ならおそらく言うだろう。コード内で属性名を用いるのであれば(そしてこれこそ属性名を一級市民扱いすることの目的なのだが)、結局はコードと属性名の結びつけを行うことになると。まさにその通りである。しかし、少なくとも今回の方法は他の方法以上に迅速なものであるうえ、汎用性が高く、属性と値との自在なカップリングを可能にする。データファイルに新しいフィールドを加えても、コードに変更を加える必要はない。少なくともそのフィールドを実際に利用するときまでは。
ともあれ、本当に重要なのは次の点である。今回のものは単なるエクササイズに過ぎない。Ruby を使ってできるメタプログラミングのほんの一例である。この強力な機能を使いこなし、これまで誰も思いもつかなかったような方法で実際に役立てていってもらいたい。