Ruby

おもしろメタプログラミング/6章

6章 金曜日: コードを記述するコード

金曜日のアジェンダ

  1. クラスマクロとの恋
  2. evalを使おうよ!
  3. eval使うのやめようよ!
  4. クラスマクロにしようよ!
  5. フックメソッドを使おうよ!

1. クラスマクロとの恋

  • そもそもクラスマクロって?
    クラス定義内でselfを明示せずに呼び出せるキーワードのようなメソッド ex) attr_accessor
  • クラスマクロとの邂逅
    亀ちゃん「クラスマクロってメソッドだったの!? すげえ、俺も独自のクラスマクロ作りてえ!」
    亀ちゃんは最近物忘れがひどく、自分がいつインスタンス変数を作ったのか忘れてしまう。そのため、インスタンス変数をハッシュにして、時間のデータを保存できる様にするクラスマクロを実装したい様だ。

  • 亀ちゃんの希望

Class Intern
  attr_with_date :shimatomo
  attr_with_date :kemako
end

intern = Intern.new
intern.shimatomo = 'shimada'
intern.kemako = 'miyatake'
intern.shimatomo[:val] # => 'shimada'
intern.shimatomo[:created_at] # => 3行上で作成した時のTime

2. evalを使おうよ!

よしけん「それじゃあまず、attr_accessorをただのメソッドとして実装してみようか。ちょうどいいから、ここでevalを使ってみよう。」
亀ちゃん「:raising_hand:

けまこ「evalは、引数として渡した文字列をコードとして実行します。詳しくはメタプロ参照。」

Q1. add_attr_with_dateメソッドの挙動は以下の様になるように実装

class Intern; end

add_attr(Intern, :shimatomo)
intern = Intern.new
intern.shimatomo = 'shimada'
puts intern.shimatomo # 'shimada'

def add_attr_with_date(klass, attr)
  eval "
    #code...
  "
end

# ヒント!
# hogeと言う名前のattrを受け取った場合に以下の様なメソッドを定義してほしい。
def hoge=(value)
  @hoge = value
end

def hoge
  @hoge
end

A1. add_attr

def add_attr_with_date(klass, attribute)
  eval "
    class #{klass}
      def #{attribute}
        @#{attribute}
      end

      def #{attribute}=(val)
        @#{attribute} = valf
      end
    end
  " 
end

# 亀ちゃんさんの希望では次の様になる
  def #{attr}=(val)
    @#{attr} = {val: val, created_at: Time.now}
  end

亀ちゃん「evalを使えば、いつもは動的に設定できないクラス名もメソッド名も、インスタンス変数名も動的に定義できるなんて!」
よしけん「だけど思い出して、大いなる力には、、?」
亀ちゃん & 馬場っち「大いなる責任が伴う!!」

けまこ「evalに渡される文字列がinputなど第三者の入力を含むとき、そのコードに悪意がある場合でもそのまま実行されてしまいます。基本的にevalを使う際には、渡されるコード文字列全てが自分の入力であることを確かめましょうね。」

3. evalを使うのやめようよ!

亀ちゃん「う〜ん、evalのリスクを完全に取り除くのは難しそうだなぁ。。」
よしけん「亀ちゃん、カレントクラスを動的に変更する方法はeval以外にもあったよね?」
亀ちゃん「あっ、水曜日にやったやつだ!」

けまこ「と言うことで、class_evalを使ってadd_attr_with_dateを変更しましょう」

A2.

def add_attr_with_date(klass, attribute)
  klass.class_eval do
    define_method "#{attribute}=" do |value|
      attr_hash = { val: value, created_at: Time.new }
      instance_variable_set("@#{attribute}", attr_hash)
    end

    define_method attribute.to_s do
      instance_variable_get("@#{attribute}")
    end
  end
end

4. クラスマクロにしようよ!

亀ちゃん「でもこんなのクラスマクロじゃない!!」
よしけん「わかったわかった笑  次はようやくクラスマクロに行こうね。」
亀ちゃん「😲 」

けまこ「ようやくクラスマクロを作るときがきました。亀ちゃんの目が爛々と輝きを増しています。」

Q3. 以下の変更点を踏まえてメソッドをクラスマクロにしよう。下のコードは期待される結果です。
- クラスを明示的に引数に渡さなくする
- メソッドを定義する場所を変える(どのクラスからでも呼び出せる様に)

class Intern
  attr_with_date :shimatomo
end

intern= Intern.new
intern.shimatomo = 'shimada'
intern.shimatomo[:val] # => 'shimada'
intern.shimatomo[:created_at] # => Timeオブジェクト

A3

class Module # もしくはClassクラス
  def add_attr_with_date(attr)

    define_method(attr) do
      instance_variable_get("@#{attr}")
    end

    define_method("#{attr}=")do |val|
      instance_variable_set("@#{attr}", {
        val: val,
        created_at: Time.now
      })
    end
  end
end

亀ちゃん「できた!!!」
よしけん「そう、ClassクラスかModuleクラスにメソッドを定義するとクラスマクロとして使えるんだね。なんでかは分かる?」
中山さん「木曜日のクラス定義でやったところですね。Rubyのオブジェクトモデルにおけるメソッドの探索の挙動は、メソッドを呼び出したレシーバの特異クラスを探索し、次にその特異クラスのsuperclassを探索し、そしてまた次のsuperclassを探索、、、と言う様に続きます。その際に注意すべきなのが、BasicObjectの特異クラスのsuperclassはClassクラスだということです。木曜日にやりましたが。つまり全てのクラス(Classクラスのインスタンス)はメソッド探索の過程でClassクラスとModuleクラスを必ず通る様になっていて、その結果として全クラス内でselfを明示することなく、キーワードの様にClassクラスとModuleクラスのインスタンスメソッドを呼び出せます。これがクラスマクロの正体ですね。ClassクラスとModuleクラスの次がObjectクラス、BasicObjectクラスが最後、という風に続きますが、これは全クラスだけでなくクラスのインスタンスオブジェクトを含む全てのオブジェクトも探索しにきてしまうためクラスマクロとしてはhogehoge.....」
のりピー「やんなぁ」

5. フックメソッドを使おうよ!

よしけん「クラスマクロを完成させたのはいいけど、このままだと全てのクラスから呼び出せてしまうよね。」
亀ちゃん「そんなのやだ!限られた相手と合意の上(includeされた上)だけにしてほしい!」
かっちゃん「亀ちゃんくんは少々束縛癖をこじらせているみたいだね。」

けまこ「そんな亀ちゃんのために、オブジェクトモデルでの様々な変化(継承、include、extend)などのイベントがあった際に特定の処理を設定できるフックメソッドがあります。これでいろんな制限や特殊な処理を設けられますね。最低限self.includedの使い方だけは理解して次の問題に臨みましょうね。」

Q4. クラス内でincludeした時にクラスメソッドが使える様になるMyModuleを作成

Class Intern
  include MyModule

  add_attr_with_date :shimatomo
end

obj = Intern.new
obj.shimatomo = 'shimada'
obj.shimatomo[:val] # => 'shimada'
obj.shimatomo[:created_at] # => Timeオブジェクト

A4.

module MyModule
  def self.included(klass)
    class << klass
      def add_attr_with_date(attr_name)
        self.class_eval do
          define_method "#{attr_name}=" do |value|
            instance_variable_set("@#{attr_name}", { val: value, created_at: Time.now })
          end
          define_method "#{attr_name}" do
            instance_variable_get("@#{attr_name}")
          end
        end
      end
    end
  end
end

亀ちゃん「self.includedメソッドの中で単純にメソッド定義するだけだとインスタンスメソッドとしてしか使えないけど、class << klass部分でklassの特異クラスをオープンしメソッド定義することで、クラスマクロを実現したんだ!」
よしけん「このフックメソッドの使い方は、実際にサーベイのコードとかにも出てくるから、覚えておくといいね。」

けまこ「これで金曜日も終わり、長かったメタプロの週に終わりが来ました。これであなたもRubyistやね!」