主夫ときどきプログラマ

データベース、Webエンジニアリング、コミュニティ、etc

Rubyでメタプログラミング

勉強会で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()の周りをラップしている。 これをアラウンドエイリアスと呼ぶ。

  1. メソッドにエイリアスをつける
  2. 新しいメソッドを定義する
  3. 新しいメソッドから古いメソッドを呼び出す

でもどんなときに使ったらいいの?

事例紹介

HTMLページを量産したい

  • 飲食チェーンの広告ランディングページ
  • 100店舗すべて別ページを作る
  • 基本デザインは同じ
  • 店舗固有のデータ(店名や住所など)はCSVで管理

テンプレートとデータを読み込んで各店舗のページを出力するようなツールが欲しい

イメージはこんな感じ

f:id:masayuki14:20160428121521p:plain

まずは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

メタプログラミングRuby