この記事は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>
内側のrefine
はrefinement:対象のクラス@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って本当にべんりですね。
では。