1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Okinawa.rbAdvent Calendar 2018

Day 6

refineをしてるモジュール取り出せるrefinements_robbery gem

Last updated at Posted at 2018-12-06

この記事はOkinawa.rb Advent Calendar 2018の6日目の記事です。
昨日は @hanachin_ さんのpendingしてからコミットpushしよう 〜RSpecしぐさ〜でした。
明日は @_atton さんのReFe + ref.vim で Neovim から Ruby の document を読むです。
明後日から20日まで、22日、23日、と25日を書く人を募集しています。

忙しい人向けの紹介

refinements_robbery gemを使うとそのクラスをrefineしているRefinementsを取り出すことが出来ます。
https://github.com/hanachin/refinements_robbery

require "refinements_robbery"

class C
  using Module.new {
    refine(C) {
      def hi
        puts "hi"
      end
    }
  }
end

# C.new.hi # NoMethodError

using *RefinementsRobbery.rob(C)

C.new.hi
# hi

きっかけ

【Ruby Advent Calendar 2018】あなたのしらない Refinements の世界【3日目】を読みました。

そこではRefinements便利事例としてこういうコードが紹介されていました。
でも、、、防ぐことができるって書いてあったら破ることもできそうじゃないですか?
イェーイ、わたしもやってみよ!

class User
  # Refinements で隠蔽したいメソッドを定義する
  # また Module.new で動的にモジュールを定義することで
  # あとから using することも防ぐ事が出来る
  using Module.new {
    refine User do
      def name
        @__name__
      end
    end
  }

  def initialize name
    @__name__ = name
  end

  def to_s
    "name is #{name}"
  end
end

class UserEx < User
  def meth
    # using してないコンテキストなので参照できない!!
    name
  end
end

homu = UserEx.new "homu"

# Error: undefined local variable or method `name' for #<UserEx:0x000055998748ee48 @__name__="homu"> (NameError)
p homu.meth

実装方法

モジュールを取ってくる

ObjectSpaceを使うと全てのオブジェクトを操作できます。

全てのオブジェクトを操作するためのモジュールです。
https://docs.ruby-lang.org/ja/latest/class/ObjectSpace.html

なまえがないRefinementsもオブジェクトなので、操作できそうですね!

こうすると全てのモジュール・クラスがとれます

ObjectSpace.each_object(Module)

対象クラスをrefineしているか?

名前を見ます

p Module.new { p refine(Object) {} }
# #<refinement:Object@#<Module:0x000055981dd1f7a8>>
# #<Module:0x000055981dd1f7a8>

内側のrefinerefinement:対象のクラス@Refinementsのモジュールみたいな名前をしてそうですね。

おもむろにgrepします。

% cd path/to/ruby
% git grep refinement:
doc/ChangeLog-2.0.0:	  a refinement, returns a string in the format #<refinement:C@M>,
doc/syntax/refinements.rdoc:Here is a basic refinement:
object.c:	VALUE s = rb_usascii_str_new2("#<refinement:");
test/ruby/test_refinement.rb:    assert_equal("#<refinement:Integer@TestRefinement::Inspect::M>",

お目当ての処理がみつかりました!

    refined_class = rb_refinement_module_get_refined_class(klass);
    if (!NIL_P(refined_class)) {
        VALUE s = rb_usascii_str_new2("#<refinement:");

        rb_str_concat(s, rb_inspect(refined_class));
        rb_str_cat2(s, "@");
        CONST_ID(id_defined_at, "__defined_at__");
        defined_at = rb_attr_get(klass, id_defined_at);
        rb_str_concat(s, rb_inspect(defined_at));
        rb_str_cat2(s, ">");
        return s;
    }
    return rb_str_dup(rb_class_name(klass));

Fiddleを使うとRubyのCの機能が呼べるので、ソースコードを見ながらなんかいい感じに実装します。
https://docs.ruby-lang.org/ja/latest/library/fiddle.html

def rb_intern_str(str)
  rb_intern_str = Fiddle::Handle::DEFAULT["rb_intern_str"]
  rb_intern_str_f = Fiddle::Function.new(rb_intern_str, [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
  rb_intern_str_f.call(Fiddle.dlwrap(str))
end

def rb_attr_get(mod, str)
  id = rb_intern_str(str)
  rb_attr_get = Fiddle::Handle::DEFAULT["rb_attr_get"]
  rb_attr_get_f = Fiddle::Function.new(rb_attr_get, [Fiddle::TYPE_VOIDP] * 2, Fiddle::TYPE_VOIDP)
  m = Fiddle.dlwrap(mod)
  Fiddle.dlunwrap(rb_attr_get_f.call(m, id))
end

def defined_at(mod)
  rb_attr_get(mod, "__defined_at__")
end

def rb_refinement_module_get_refined_class(mod)
  rb_attr_get(mod, "__refined_class__")
end

あとはこれをつかって絞り込むだけ! かんたんだ〜。

def rob(klass)
  ObjectSpace.each_object(Module).select {|m|
    # 対象のクラスをrefineしているか調べる
    rb_refinement_module_get_refined_class(m) == klass
  }.map {|m|
    # refineをまとめているRefinementsのモジュールをたどる
    defined_at(m)
  }.uniq
end

実践

さきほどの【Ruby Advent Calendar 2018】あなたのしらない Refinements の世界【3日目】のコードで試してみましょう。

class User
  # Refinements で隠蔽したいメソッドを定義する
  # また Module.new で動的にモジュールを定義することで
  # あとから using することも防ぐ事が出来る
  using Module.new {
    refine User do
      def name
        @__name__
      end
    end
  }

  def initialize name
    @__name__ = name
  end

  def to_s
    "name is #{name}"
  end
end

class UserEx < User
  def meth
    # using してないコンテキストなので参照できない!!
    name
  end
end

homu = UserEx.new "homu"
# p homu.meth # undefined local variable or method `name' for #<UserEx:0x0000563939f98bc0 @__name__="homu"> (NameError)

require "refinements_robbery"

class UserEx < User
  # UserのRefinementsをあとから using する
  using *RefinementsRobbery.rob(User)

  def meth2
    # using しているコンテキストなので参照できる!!
    name
  end
end

homu = UserEx.new "homu"
p homu.meth2
# "homu"

無事、あとからusingできました!

まとめ

はい

  • RefinementsRobberyを使うとクラスをrefineしているRefinementsを取り出せてべんり
  • RefinementsRobberyの実装方法がわかった
    • ObjectSpaceを使うと全てのオブジェクトを操作できてべんり
    • Fiddleを使うとRubyのCのコードが呼べてべんり

なまえない
無名モジュール
とりだせる

Rubyって本当にべんりですね。

では。

1
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?