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

この記事は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って本当にべんりですね。

では。