Table of Contents
Open Table of Contents
しっかり抑えておきたいと思うメタプログラミング
Ruby のメタプロミングは奥が深くて楽しいが、やり過ぎると暗黒面に堕ちてしまう。。
効果的な使い方を心掛けていきたい。
Rails でもよく利用されているように、CoC (Convention over Configuration) と組み合わせることで更に効果的となる。
なかなかとっつきにいところもあり、コンパクトにまとまった文書がないかと探していた。
上記サイトなどはコンパクトによくまとまっているように思う。
メタプログラミングに必要となる、通常はあまり使用しないメソッドをメタプログラミングツールボックスというかたちでリストアップし、その説明はドキュメントへのレファレンスで構成している。 最後にメタプログラミングの適用例として ActiveRecord の簡易実装をサンプルとして掲載している。
記載されているツールボックスの利用の前に、基本となる、
- Ruby のクラス階層
Object クラス、Module クラス、Class クラスと任意のクラスとの関連。 - メソッド参照のルール
もしっかり抑えておく必要があるようには思う。その方が混乱が少なくて済む。。
個人的にはメタプログミングと言えば、why の Metaid が思い付くのだが、why はもう姿を現してくれないのだろうか・・・
“why’s (poignant) guide to ruby” が以下のサイトで再掲されているようだ。
時間を見つけて以下のサイトを読み込んでみるのも良さそうだ。
また、“Metaprogramming Ruby” は非常に楽しみしている書籍で、まだベータブックではあるが、The Pragmatic Bookshelf | Metaprogramming Ruby を購入して読んでみようと思う。
と、今回のこのメモの中では、先に挙げた Ruby’s Metaprogramming Toolbox で掲載されている ActiveRecord の簡易実装サンプルを SQLite 版で書いてみて、使用されているツールボックスのツールを確認してみたいと思う。
ActiveRecord の簡易実装 (ちょっとした検索だけ)
Ruby’s Metaprogramming Toolbox では最後にメタプログミングの適用例として、
ActiveRecord の簡易実装を記載している。
これが MySQL 用のものであるので、もっと手軽に試せる SQLite 版にしてみた。
require 'rubygems'
require 'sqlite3'
# for SQLite
class PoorManWithSqlite
class << self; attr_reader :generated_classes; end
@generated_classes = []
def initialize(attributes = nil)
if attributes
attributes.each_pair do |key, value|
instance_variable_set('@' + key, value)
end
end
end
def self.connect(database = 'data.db')
@@db = SQLite3::Database.new(database)
sql = <<-SQL
select tbl_name
from sqlite_master
where type = 'table'
and tbl_name != 'sqlite_sequence'
order by tbl_name;
SQL
@@db.execute(sql).flatten.each do |table_name|
class_name = table_name.split('_').map { |word| word.capitalize }.join
@generated_classes << klass = Object.const_set(class_name,
Class.new(PoorManWithSqlite))
klass.module_eval do
@@fields = []
@@table_name = table_name
def fields; @@fields; end
end
@@db.table_info(table_name) do |field|
klass.send :attr_accessor, field['name'].to_sym
klass.module_eval { @@fields << field['name'] }
end
end
@@db
end
def self.close
@@db.close
end
def self.find(id)
result = @@db.execute2("select * from #{@@table_name} where id = #{id} limit 1")
attributes = Hash[*result.transpose.flatten]
new(attributes) if attributes
end
def self.all
results = @@db.execute2("select * from #{@@table_name}")
fields = results.shift
records = []
results.each do |result|
records << new(Hash[*fields.zip(result).flatten])
end
results
end
end
PoorManWithSqlite.connect
table_classes = PoorManWithSqlite.generated_classes # => [Departments, Employees]
table_classes[0].superclass # => PoorManWithSqlite
employee = Employees.find(1) # => #<Employees:0x11f20d8
@department_id=nil,
@name="taka",
@id="1">
employee.name # => "taka"
employees = Employees.all # => [["1", "taka", nil],
["2", "goro", nil]]
PoorManWithSqlite.close # => true
上記の実行にあたっては、簡単な DB を用意しておく必要がある。
create-table.sql
ファイルの内容は以下の通り。
create table departments (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
name varchar(255)
);
create table employees (
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
name varchar(255),
department_id integer
);
data.db
を作成し、上記 DDL 文を流し込む。
$ sqlite3 data.db < creat-table.sql
ポイントメモ
幾つかポイントをメモ。
- 動的なクラスの生成(しかもサブクラス)
- 動的なアクセサの生成とコンストラクタによる値のセット
- クラスインスタンス変数の利用とそのアクセサの生成
Object#send
を使った private メソッドへのアクセス
1. 動的なクラスの生成(しかもサブクラス)
Module#const_set
を利用して、テーブルの名前から動的に PoorManWithSqlite
クラスのサブクラスとなる各テーブルのクラスを生成している。
@generated_classes << klass = Object.const_set(class_name,
Class.new(PoorManWithSqlite))
Module#const_set(name, value) -> object
モジュールに name で指定された名前の定数を value という値に定義し、value を返す。
C = Object.const_set('Con', 10)
Con # => 10
C # => 10
Ruby の場合、クラス名は全て定数となる。name にテーブル名から生成した名称のクラス名を指定し、
value には、PoorManWithSqlite
のサブクラスを指定している。
その value の指定に使用されているのは Class::new
メソッド。Class の new メソッドには、インスタンスメソッドとクラスメソッドがある。
上記で利用しているのはクラスメソッド(Class::new
)の方になる。
Class::new(superclass = Object) -> Class
Class::new(superclass = Object) {|klass| ... } -> Class
新しく superclass の無名のサブクラスを生成する。
class SuperAClass; end
sub_k = Class.new(SuperAClass) # => #<Class:0x2533c>
sub_k.class # => Class
sub_k.superclass # => SuperAClass
コードブロックを預けてメソッドを定義することもできる。
k = Class.new(SuperAClass) do |klass|
def hoge
'hoge hoge'
end
end
o = k.new # => #<#<Class:0x273e4>:0x273a8>
o.hoge # => "hoge hoge"
各テーブルのクラスを PoorManWithSqlite
クラスのサブクラスとすることで、各テーブルクラスにおいて PoorManWithSqlite
の機能が利用できる。
2. 動的なアクセサの生成とコンストラクタによる値のセット
動的なアクセサを用意しているところ。
@@db.table_info(table_name) do |field|
klass.send :attr_accessor, field['name'].to_sym
klass.module_eval { @@fields << field['name'] }
end
テーブルのフィールドをイテレートし、そのテーブルクラスの読み書き両用のインスタンス変数を生成している。
次に値をセットしているところ。
def initialize(attributes = nil)
if attributes
attributes.each_pair do |key, value|
instance_variable_set('@' + key, value)
end
end
end
コンストラクタで引数にフィールド名を key、そのフィールドの値を value とした Hash を受け取り、
Object#instance_variable_set
で値のセットを行う。'@'
を文字列連結しているところはちょっとカッコ悪いが致し方ない。
上記 initialize
メソッドが利用される実際に値をセットしているところは以下の箇所となる。
results.each do |result|
records << new(Hash[*fields.zip(result).flatten])
end
Hash[*fields.zip(result).flatten]
は検索結果の配列からフィールド名を key、そのフィールドの値を value とした Hash を生成している。
3. クラスインスタンス変数の利用とそのアクセサの生成
各テーブルクラスは、PoorManWithSqlite
クラスを継承する。
継承の際のクラス変数の扱いには注意が必要だ。
1.8 系まではクラス変数は継承される(1.9 はまだ触っていない。。)。
Database への接続の参照は唯一のインスタンスとして持たせたいので、クラス変数が適している。
ただ、作成されたテーブルクラスの管理情報を各テーブル毎に持たせても仕方ない。このサンプルでは PoorManWithSqlite
クラスのクラスインスタンス変数にその情報を保持している。
class << self; attr_reader :generated_classes; end
@generated_classes = []
アクセサは、PoorManWithSqlite
クラスオブジェクトの特異クラスで記述する必要がある。
4. Object#send
を使った private メソッドへのアクセス
@@db.table_info(table_name) do |field|
klass.send :attr_accessor, field['name'].to_sym
klass.module_eval { @@fields << field['name'] }
end
klass
というのは、PoorManWithSqlite
を継承したテーブル毎のクラスなのだが、このクラスの attr_accessor
メソッドには、klass
をレシーバとしてはアクセスできない。
Object#send
を使うことにより、アクセスコントロールをバイパスしている。
例示のため使っているのだと思うが、その下にある Module#module_eval
のブロックの中で一緒に書いてもよいと思う。
klass.module_eval { attr_accessor field['name'].to_sym; @@fields << field['name'] }