読者です 読者をやめる 読者になる 読者になる

たごもりすメモ

コードとかその他の話とか。

Rubyでdynamic scopeを(メソッド定義だけ)実現する dyna_mo を書いた

ついカッとなって書いた。たのしかった。

https://github.com/tagomoris/dyna_mo

どういうことかというと、つまりこういうことだ!

require 'dyna_mo'
 
module MyModule
  class MyClass
    attr_accessor :num
    def initialize; @num = 0; end
    def name; "name"; end
    def sum(numbers); @num + numbers.reduce(:+); end
  end
end

こういう割と普通なクラスに対して

dynamo_define('MyModule::MyClass', :mytest_case_default) do
  def_method(:initialize) do
    @num = 1
  end
  
  def_method(:name) do  # #name を差し替える
    "dummyname"
  end
  
  def_instance_method(:name, :mytest_case1) do # #name 差し替えの別パターン
    "dummyname1"
  end
  
  def_method(:sum) do |numbers|
    @num + numbers.reduce(:+) + 1
  end
  
  def_class_method(:create) do |init_num=0| # 元の定義になかった MyClass.create の追加
    obj = self.new
    obj.num = init_num
    obj
  end
end

特定のとき*1にこのメソッドをこういうふうに上書きしてね!*2 ということを指定して

class SynopsisTest < Test::Unit::TestCase
  def test_synopsis
    assert { MyModule::MyClass.new.name == "name" } # 元の状態

    obj = MyModule::MyClass.new
    
    assert { obj.num == 0 }  # インスタンス変数 @num は 0
    assert { obj.name == "name" }
    
    dynamo_context(:mytest_case_default) do # ここから定義をスイッチ
      assert { obj.num == 0 } # #initialize is not overridden # これは @num の参照だけで変更なし
      
      assert { MyModule::MyClass.new.name == "dummyname" } # ウヒョー!
      assert { obj.name == "dummyname" } # 生成済みのオブジェクトのメソッドだってもちろん変わる!
      
      assert { MyModule::MyClass.new.num == 1 } # initializeだって上書きできる!
      
      assert { MyModule::MyClass.new.sum([1,2,3]) == (1+(1+2+3)+1) } # 普通に引数だって与えられる!
      
      assert { MyModule::MyClass.create(100).num == 100 } # ちょっとこのテスト専用に便利メソッド!
    end
  end

こういう感じで使う! dynamo_context(:mytest_case_default) に与えているブロックの中でだけ MyClass#name や MyClass#sum の動きが違ったり MyClass.create が使えたりする!!!!!!!!!!!!!!!!!!!
コンテキスト外で作った obj のインスタンス変数 @num は変化していないがそれを使うメソッドだけが書き変わっている! ウヒー!

もちろん別のコンテキスト名を指定すれば別の動きかたをする!

class SynopsisTest < Test::Unit::TestCase
  def test_synopsis
    dynamo_context(:mytest_case1) do # もちろん違うコンテキストでは違う動きになる!
      assert { obj.name == "dummyname1" }
    end
    
    dynamo_define(MyModule::MyClass, :onetime_context) do
      def_method(:name) do
        "onetime"
      end
    end
    
    dynamo_context(:onetime_context) do # 違う! 動きに!!!
      assert { obj.name == "onetime" }
    end
  end
end

もちろんだがダイナミックスコープなので、直接記述されていなくても、与えられたブロックからの呼び出し階層のどこかに存在するメソッドであれば容赦なく書き変わっている。例えば次のコードは MyClass を継承して MyClass2#name を呼んでいるが、親クラスの宣言は :onetime_context 内では書き変わっているので自動的に MyClass2#name の結果も変わる! カッコイイ!!!!!!

module MyModule; class MyClass2 < MyClass; end; end
 
class Synopsis2Test < Test::Unit::TestCase
  def test_onece_more
    dynamo_define(MyModule::MyClass, :onetime_context) do
      def_method(:name) do
        "onetime"
      end
    end
    
    dynamo_context(:onetime_context) do
      assert { MyModule::MyClass2.new.name == "onetime" } # 継承元の書き換えがここにも影響!
    end
  end
end

このような感じで自由の地平が開けます。ぜひどうぞ。

FAQ

なんで作ったの?

RubyKaigiの質問コーナーでdynamic scope欲しい!って言ったらMatzに「dynamic scopeを使わないためにRubyを作っている」と答えられたのでムシャクシャしてやった。

なんで作ったの?

power_assert とかちょっと真面目に使ってみたかった。が、まだちょっとバグいね*3

なんで作ったの?

RubyHirobaでうまい方法ないかなーと思っていたら @_ko1 さんにスレッドローカル変数とModule#prependを使うという方法を授かったのでついやってしまった。責任の半分は @_ko1 さんにあると思う。

本番で使っていい?

絶対にやめましょう。ありとあらゆる不幸が振りかかります。

本番で使っちゃおうかな?

万が一にでも自分が数年後とかに転職した先で見付けた本番コードで使われてたりしたら、たとえ退職者であろうとも書いた人のリアル住所を割って襲撃に行くかもしれません。やめておいた方が賢明です。

ぶっちゃけこれ必要なのって設計が悪いんじゃない?

はい。

例えばマルチスレッドをゴリゴリ使いまくっているのに超大事なところにシングルトンオブジェクトが居座っているFluentdというソフトウェアなんかがあって、そいつがマジでもうどうにもならなかったりするので作りました。しかしFluentd v0.12 ではそのシングルトンオブジェクトが排除されたため不要になりました。おおお……。。。

まあ、しかし、外部gemの動作に依存したコードのテストを書かなければならない時ってあるじゃないですか。ナントカAPIとかナントカSDKみたいなgemを叩きたいんだけどそいつがバックエンドをモックに置き換えられなくなってるとか! あるでしょ!

たのしかった?

はい!!!!!!!!!!!!!! 特異クラスに対してprependとかしてて脳から変な汁が出そうになった!!!

*1:この場合は :mytest_case_default や :mytest_case1 を指定したときだけ

*2:あるいは追加してね!

*3:変なことばっかりやってるテストコードなので再現条件が難しい、が、何がどうfailしたのかがたまに出なかったりした。そのうちちゃんと調べてフィードバックしたい。