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

この記事は[Okinawa.rb Advent Calendar 2018](https://qiita.com/advent-calendar/2018/okinawarb)の6日目の記事です。
昨日は @hanachin_ さんの[pendingしてからコミットpushしよう 〜RSpecしぐさ〜](https://qiita.com/hanachin_/items/bfce007529c962c5ed74)でした。
明日は @_atton さんの[ReFe + ref.vim で Neovim から Ruby の document を読む](https://attonblog.blogspot.com/2018/12/okinawarb-advent-calendar-2018-12-07.html)です。
明後日から20日まで、22日、23日、と25日を書く人を募集しています。

## 忙しい人向けの紹介

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

```rb
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日目】](http://secret-garden.hatenablog.com/entry/2018/12/03/231925)を読みました。

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

```rb
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もオブジェクトなので、操作できそうですね!

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

```rb
ObjectSpace.each_object(Module)
```

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

名前を見ます

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

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

おもむろにgrepします。

```rb
% 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>",
```

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


```c
    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

```rb
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
```

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

```rb
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日目】](http://secret-garden.hatenablog.com/entry/2018/12/03/231925)のコードで試してみましょう。

```rb
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って本当にべんりですね。

では。