勉強会で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
ActiveRecord はテーブルを知ってるの?
知りません
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*?") # => "hello its magic number 3"
でもこのメソッドはオブジェクト指向的じゃないよね?
文字列自身に変換してもらおう
class String def to_alphanumeric gsub /[^\w\s]/, '' end end "#hello, it's magic number 3*?".to_alphanumeric() # => "hello its magic number 3"
いつだってクラスを再オープンできる
class A def x; 'x'; end end class A def y; 'y'; end end obj = A.new obj.x # => 'x' obj.y # => 'y'
class
キーワードはクラス宣言というよりはスコープ演算子のようなもので、class
を使ってクラスコンテキストに行きそこでメソッドを定義する。つまりいつでもクラスを再オープンしてその場で修正ができる。
モンキーパッチ
同じ要領で配列要素を置換するメソッドを定義しよう
def replace(array, from, to) array.each_with_index do |e, i| array[i] = to if e == from end end
# Arrayクラスに再定義 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) # => 30
Object#send()
メソッドでも呼び出すことができる。
obj.send(:saba, 45) # => 40
動的ディスパッチ
obj.saba(35) # => 30 obj.send(:saba, 45) # => 40
どちらのコードも 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 # => "cheese !" p.formaggio # => "cheese !" p.pomodori # => "tomato!"
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 # => "long" "I'm so happy!".real_length # => 13
このコードはString#length()
を再定義している。しかしエイリアスは元のメソッドを参照している。
メソッドの再定義とは、元のメソッドを変更するのではなく、新しいメソッドを定義して、元のメソッドの名前をつけているわけだ。
アラウンドエイリアス
class Fixnum alias :old_plus :+ def +(value) self.old_plus(value).old_plus(1) end end 3 + 5 # => 9 4 + 10 # => 15
新しい +()
が 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 # => 102 row.store_name # => おばけベーカリー 新宿店 row.address # => 新宿区新宿2丁目・・・
オープンクラスと動的メソッド
CSVファイルの1行目(項目名)を使ってアクセッサを動的に定義してやればいい!
class SpooRecord ### オープンクラスの同類 # 特定のインスタンスを再オープンして定義を追加します # この中で定義されたメソッドはクラスメソッドになります class << self ### 属性の定義 # attributes = ['store_id', 'store_name'] の場合 # store_id, store_id=, store_name, store_name= のアクセッサを定義する def define_attributes(attributes) attributes.each do |attr| attr_accessor attr # 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 => #<SpooRecord:0x007ffcc1046378> 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 #<SpooRecord:0x007ffcc1046378 @id=100, @name="Spookies", @address="京都市"> => #<SpooRecord:0x007ffcc1046378 @id=100, @name="Spookies", @address="京都市"> 2.2.1 :026 >
動的ディスパッチで値を設定する
2行目以降のデータを対応する項目値として設定する
class SpooRecord class << self # indexと項目名の対応を保持, 1 => store_id # クラスにアクセッサが定義される # SpooRecord.index_to_attr にアクセスできる attr_accessor :index_to_attr def define_attributes(attributes) @index_to_attr ||= {} attributes.each_with_index do |attr, index| attr_accessor attr # attrへのアクセッサ @index_to_attr[index] = attr # indexと項目名の対応 end end end # コンストラクタ def initialize(data) data.each_with_index do |value, index| attr = self.class.index_to_attr[index] # クラスメソッドの `index_to_attr` で読込 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', '京都市'] ) => #<SpooRecord:0x007fd9c10945c8 @id=100, @name="Spookies", @address="京都市"> 2.2.1 :025 > p r #<SpooRecord:0x007fd9c10945c8 @id=100, @name="Spookies", @address="京都市"> => #<SpooRecord:0x007fd9c10945c8 @id=100, @name="Spookies", @address="京都市">
出力は 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