勉強会でRubyについて話す機会があったのでその資料を公開します。
勉強会についてのエントリはこちら。
スプーキーズの勉強会イベント一般公開します。 - スプーキーズの中の人。
コードを記述するコードを記述すること
メタプログラミング (metaprogramming) とはプログラミング技法の一種で、ロジックを直接コーディングするのではなく、あるパターンをもったロジックを生成する高位ロジックによってプログラミングを行う方法、またその高位ロジックを定義する方法のこと。主に対象言語に埋め込まれたマクロ言語によって行われる。
by wikipedia
It's Magic!
Rubyを使ってDBにアクセスするプログラムを書いてみよう。
今回書くのはActiveRecord
を使ってmovies
テーブルにアクセスするクラスだ。
class Movie < ActiveRecord::Base
end
Wow, たったこれだけ!
movie = Movie.new
movei.title = '燃えよドラゴンズ!'
movie.title
movie.save
知りません
Movie#title()
や Movie#title=()
を呼び出しているが、これらのソースコードはどこにもない。
どこにも定義されてないとするとどうやって存在するんだい?
これこそメタプログラミングがやってくれるんだ!!
実行時にメソッドを定義している
ActiveRecord
は実行時にクラス名を調べ簡単な規約を適用します。
例えば Movie
クラスであれば movies
テーブルにマッピングします。
そしてデータベーススキーマを読み取り title
カラムがあることを見つけ、この属性のアクセッサメソッドを定義しています。
プログラムの実行中にこっそりと定義してるのだ!!
魔法のようなメタプログラミングをするには魔術書が必要!
文字列からアルファベットと数字以外を除外したい。
def to_alphanumeric(s)
s.gsub /[^\w\s]/, ''
end
to_alphanumeric("#hello, it's magic number 3*?")
でもこのメソッドはオブジェクト指向的じゃないよね?
文字列自身に変換してもらおう
class String
def to_alphanumeric
gsub /[^\w\s]/, ''
end
end
"#hello, it's magic number 3*?".to_alphanumeric()
いつだってクラスを再オープンできる
class A
def x; 'x'; end
end
class A
def y; 'y'; end
end
obj = A.new
obj.x
obj.y
class
キーワードはクラス宣言というよりはスコープ演算子のようなもので、class
を使ってクラスコンテキストに行きそこでメソッドを定義する。つまりいつでもクラスを再オープンしてその場で修正ができる。
モンキーパッチ
同じ要領で配列要素を置換するメソッドを定義しよう
def replace(array, from, to)
array.each_with_index do |e, i|
array[i] = to if e == from
end
end
class Array
def replace(from, to)
each_with_index do |e, i|
self[i] = to if e == from
end
end
これをプロダクトのコードでやったらおかしなことになってしまうぞ!
既存のメソッドを上書きしてしまった
> [].methods.grep /^re/
=> [:reverse_each, :reverse, :reverse!, :reject, :reject!, :replace, .....
独自の replace()
メソッドでうっかり元の replace()
メソッドを上書きしてしまった。
こうしたクラスへの安易なパッチに否定的な人には蔑称で モンキーパッチ って言われちゃうぞ!
これが オープンクラスのダークサイドだ。
メソッドを知る
Rubyでは動的なメソッド呼び出しやメソッド定義が可能だ。
Object#send() メソッド
メソッドを呼び出すには通常ドット記法を使う。
class Person
def saba(my_age)
my_age - 5
end
end
obj = Person.new
obj.saba(35)
Object#send()
メソッドでも呼び出すことができる。
obj.send(:saba, 45)
動的ディスパッチ
obj.saba(35)
obj.send(:saba, 45)
どちらのコードも saba()
を呼び出してるが、後者は send()
を使っている。これはメソッドで第1引数はメソッド名、その他の引数(とブロック引数)はそのままメソッドに渡される。
つまりメソッドを実行するメソッドというわけだ。
呼び出したいメソッド名が通常の引数になるから実行時に呼び出すメソッドを直前に決められる。これを動的ディスパッチと呼ぶ。
メソッドを動的に定義する
Module#define_method()
を使えば、メソッドをその場で定義することができる。メソッド名とブロックを渡す必要があり、ブロックがメソッドの本体となる。
class Spookies
define_method :muscle do |name|
"#{name}, さぁ今日も筋トレだ!"
end
end
spoo = Spookies.new
spoo.muscle("アッシー")
動的メソッド
通常def
キーワードを使って定義するメソッドをModule#define_method()
で定義している。これはメソッドを定義するメソッドだ。
define_method()
がSpookies
のなかで実行され、muscle()
がSpookies
のインスタンスメソッドとして定義される。実行時にメソッドを定義するこのテクニックは動的メソッドと呼ばれる。
エイリアスのメソッドもまたメソッドだ
def deserves_a_look?(book)
amazon = Amazon.new
amazon.reviews_of(book).size > 20
end
例外処理が考慮されてないメソッドをなんとかしたい。
こんな時はこのメソッドをラップして、機能を追加して、全てのクライアントが新しく追加した機能を自動的に使えるようにしたらいい。
alias
キーワードを使えば、
Rubyのメソッドにエイリアス(別名)をつけられる。
class Pizza
def cheese
'cheese !'
end
alias :formaggio :cheese
def tomato; 'tomato!'; end
alias pomodori tomato
end
p = Pizza.new
p.cheese
p.formaggio
p.pomodori
alias
はキーワード
- 新しい名前が前、古い名前が後でカンマ不要
- シンボルでもいいし素の名前でもいい
- メソッドではない
- 同じ動きをする
Module#alias_method()
メソッド
String#size()
はString#length
のエイリアス
メソッドを alias
して再定義すると何が起きるだろうか?
class String
alias :real_length :length
def length
real_length > 5 ? 'long' : 'short'
end
end
"I'm so hungry".length
"I'm so happy!".real_length
このコードはString#length()
を再定義している。しかしエイリアスは元のメソッドを参照している。
メソッドの再定義とは、元のメソッドを変更するのではなく、新しいメソッドを定義して、元のメソッドの名前をつけているわけだ。
class Fixnum
alias :old_plus :+
def +(value)
self.old_plus(value).old_plus(1)
end
end
3 + 5
4 + 10
新しい +()
が old_plus()
の周りをラップしている。
これをアラウンドエイリアスと呼ぶ。
- メソッドにエイリアスをつける
- 新しいメソッドを定義する
- 新しいメソッドから古いメソッドを呼び出す
でもどんなときに使ったらいいの?
事例紹介
HTMLページを量産したい
- 飲食チェーンの広告ランディングページ
- 100店舗すべて別ページを作る
- 基本デザインは同じ
- 店舗固有のデータ(店名や住所など)はCSVで管理
テンプレートとデータを読み込んで各店舗のページを出力するようなツールが欲しい
イメージはこんな感じ
まずはCSVを読み込もう
store_id |
store_name |
address |
102 |
おばけベーカリー 新宿店 |
新宿区新宿2丁目・・・ |
103 |
おばけベーカリー 渋谷店 |
渋谷区宇田川町・・・ |
110 |
おばけベーカリー 池袋店 |
豊島区東池袋1丁目・・・ |
line_number, rows = [ 0, {} ]
CSV.foreach('data.csv') do |line|
line_number += 1
rows << {
store_id: line[0],
store_name: line[1],
address: line[2],
}
end
これってイケてないよね
- データの追加や順番が変わるたびにコードの修正が必要
store_id
などの項目名が生かせてない
こんな感じにしたい!
row = SupponRecord.new(line)
row.store_id
row.store_name
row.address
CSVファイルの1行目(項目名)を使ってアクセッサを動的に定義してやればいい!
class SpooRecord
class << self
def define_attributes(attributes)
attributes.each do |attr|
attr_accessor attr
end
end
end
end
irb
で実行してみよう
2.2.1 :020 > SpooRecord.define_attributes( ['id', 'name', 'address'] )
=> ["id", "name", "address"]
2.2.1 :021 > r = SpooRecord.new
=>
2.2.1 :022 > r.id = 100
=> 100
2.2.1 :023 > r.name = 'Spookies'
=> "Spookies"
2.2.1 :024 > r.address = '京都市'
=> "京都市"
2.2.1 :025 > p r
=>
2.2.1 :026 >
動的ディスパッチで値を設定する
2行目以降のデータを対応する項目値として設定する
class SpooRecord
class << self
attr_accessor :index_to_attr
def define_attributes(attributes)
@index_to_attr ||= {}
attributes.each_with_index do |attr, index|
attr_accessor attr
@index_to_attr[index] = attr
end
end
end
def initialize(data)
data.each_with_index do |value, index|
attr = self.class.index_to_attr[index]
self.send("#{attr}=", value)
end
end
end
irb
で実行してみよう
2.2.1 :023 > SpooRecord.define_attributes( ['id', 'name', 'address'] )
=> ["id", "name", "address"]
2.2.1 :024 > r = SpooRecord.new( [100, 'Spookies', '京都市'] )
=>
2.2.1 :025 > p r
=>
出力は ERB へバインド
例えばテンプレートがERBだったらそのままバインドしちゃいましょう
<body>
<ul>
<li>店舗: <%= store_name %></li>
<li>住所: <%= address %></li>
</ul>
</body>
class SpooRecord
def bind_erb(template_path)
File.open(template_path, 'r') do |file|
yield(ERB.new(file.read).result(binding))
end
end
end
SpooRecord を使ってみた
line_number = 0
CSV.foreach('data.csv') do |line|
if line_number == 1
SupponRecord.define_attributes( line )
next
end
row = SupponRecord.new(line)
row.bind_erb( 'template.erb') do |binded|
File.open("#{row.store_id}/index.html", 'w') { |file| file.write(binded) }
end
end
他にもいろいろ機能をついかしていくと。。。
https://gist.github.com/masayuki14/9d1e2aace3b75369c6ec5d15cdd6a642