Monkey-patching graciously
I am going to show how to monkey-patch graciously using Ruby.
The original idea is to implement a method Module#def_after
so that I can easily make something to be done after the original method. Like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Foo
def bar
print 'before'
end
end
class Foo
def_after :bar do
puts ' & after'
end
end
Foo.new.bar # => before & after
|
The implementation is a little easy:
1 2 3 4 5 6 7 8 9 |
class Module
def def_after method_name, &refine_block
old = instance_method method_name
define_method method_name do |*args, **opts, &block|
old.bind_call self, *args, **opts, &block
refine_block.(*args, **opts, &block)
end
end
end
|
However, there is a little problem. The self
in refine_block
depends on how and where refine_block
is defined instead of just being the instance receiving the method.
Since an instance method (UnboundMethod
object) defined in a Module
can bind
any other object, we can use Module#define_method
and send refine_block
as a block parameter, and then bind the instance method to self
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
class Proc
def bind receiver
Module.new.module_exec self do |block|
instance_method define_method :_, &block
end.bind receiver
end
def bind_call receiver, *args, **opts, &block
bind(receiver).(*args, **opts, &block)
end
end
class Module
def def_after method_name, &refine_block
old = instance_method method_name
define_method method_name do |*args, **opts, &block|
old.bind_call self, *args, **opts, &block
refine_block.bind_call self, *args, **opts, &block
end
end
end
|
The self
can successfully be converted. You can test it yourself.
Here is still a problem. When the new instance method is defined, its visibility is public
, while the original visibility may be private
or protected
.
Use the following means to get the visibility beforehand and set the visibility afterward:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
class Module
def method_visibility method_name
%i[public protected private].find do |visibility|
__send__ :"#{visibility}_method_defined?", method_name
end
end
def def_after method_name, &refine_block
visibility = method_visibility method_name
old = instance_method method_name
define_method method_name do |*args, **opts, &block|
old.bind_call self, *args, **opts, &block
refine_block.bind_call self, *args, **opts, &block
end
__send__ visibility, method_name
end
end
|
There can be some improvement. If we need to refine a singleton method, calling def_after
on its singleton_class
will lead to calling obj.singleton_class.instance_method(sym).bind_call(obj, *)
, which is way too complex. The straightforward way to do it is to call obj.method(sym).call(*)
.
With this inspiration, we can implement Object#def_after
:
1 2 3 4 5 6 7 8 9 10 11 |
class Object
def def_after method_name, &refine_block
visibility = singleton_class.method_visibility method_name
old = method method_name
define_singleton_method method_name do |*args, **opts, &block|
old.(*args, **opts, &block)
refine_block.bind_call self, *args, **opts, &block
end
singleton_class.__send__ visibility, method_name
end
end
|
Then there comes a new problem. A Module
also has singleton methods, while Module#def_after
can only change its instance methods instead of its singleton methods. The way to solve this is to judge whether is_a? Module
in Object#def_after
, and add a keyword argument singleton
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
class Object
def def_after method_name, singleton: false, &refine_block
singleton ||= !is_a?(Module)
# mod: the module containing the old method
# get_method: the method to get the Method/UnboundMethod obj
# def_method: the method to define a new method
mod, get_method, def_method = singleton ?
[singleton_class, method(:method), method(:define_singleton_method)] :
[self, method(:instance_method), method(:define_method)]
# get visibility
visibility = mod.method_visibility method_name
# get old
old = get_method.(method_name)
# override
def_method.(method_name) do |*args, **opts, &block|
old = old.bind self unless old.is_a? Method
old.(*args, **opts, &block)
refine_block.bind_call *args, **opts, &block
end
# set visibility
mod.__send__ visibility, method_name
end
end
|
What about parsing a callable object as an argument instead of through refine_block
? Parsing a Symbol
can also be useful. Like this:
1 |
Object.def_after :display, :puts
|
Then Object#def_after
will be a little complex:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
class Object
# pat: when refine_block is nil, it is used to represent a refinement
# singleton: force singleton when self is a Module
def def_after method_name, pat = nil , singleton: false, &refine_block
singleton ||= !is_a?(Module)
# mod: the module containing the old method
# get_method: the method to get the Method/UnboundMethod obj
# def_method: the method to define a new method
mod, get_method, def_method = singleton ?
[singleton_class, method(:method), method(:define_singleton_method)] :
[self, method(:instance_method), method(:define_method)]
# get visibility
visibility = mod.method_visibility method_name
# get pat
pat = refine_block || {
to_sym: ->symbol { get_method.(symbol.to_sym) },
to_proc: :to_proc.to_proc,
call: ->callable { callable.method :call }
}.each do |duck, out|
break out.(pat) if pat.respond_to? duck
end
# get old
old = get_method.(method_name)
# override
def_method.(method_name) do |*args, **opts, &block|
# bind old
old = old.bind self unless old.is_a? Method
# bind pat
pat = pat.bind self unless pat.is_a? Method
# call the new method
old.(*args, **opts, &block)
pat.(*args, **opts, &block)
end
# set visibility
mod.__send__ visibility, method_name
end
end
|
We are still not satisfied. We need to define a lot of methods like def_after
, such as def_before
, def_if
… Maybe we should define Object::def_
and use it like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Object
# use this binding to eval to avoid excessive local variables
def self.class_binding
binding
end
{
after: 'result = old.(*); pat.(*); result',
after!: 'old.(*); pat.(*)',
before: 'pat.(*); old.(*)',
with: 'pat.(old.(*), *)',
chain: 'pat.(old, *)',
and: 'old.(*) && pat.(*)',
or: 'old.(*) || pat.(*)',
if: 'pat.(*) && old.(*)',
unless: 'pat.(*) || old.(*)'
}.each do |sym, code|
str = "def_(:#{sym}) { |old, pat, *| #{code} }"
str.gsub! ?*, '*args, **opts, &block'
class_binding.eval str
end
singleton_class.undef_method :def_, :class_binding
end
|
The main difficulty is to implement Object::def_
. We can accomplish this just by editing the Object#def_after
we defined before:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
class Object
# the method is going to be undefined soon
def self.def_ sym, &def_block
# pat: when refine_block is nil, it is used to represent a refinement
# singleton: force singleton when self is a Module
define_method :"def_#{sym}" do |method_name, pat = nil, singleton: false,
&refine_block|
singleton ||= !is_a?(Module)
# mod: the module containing the old method
# get_method: the method to get the Method/UnboundMethod obj
# def_method: the method to define a new method
mod, get_method, def_method = singleton ?
[singleton_class, method(:method), method(:define_singleton_method)] :
[self, method(:instance_method), method(:define_method)]
# get visibility
visibility = mod.method_visibility method_name
# get pat
pat = refine_block || {
to_sym: ->symbol { get_method.(symbol.to_sym) },
to_proc: :to_proc.to_proc,
call: ->callable { callable.method :call }
}.each do |duck, out|
break out.(pat) if pat.respond_to? duck
end
# get old
old = get_method.(method_name)
# override
def_method.(method_name) do |*args, **opts, &block|
# bind old
old = old.bind self unless old.is_a? Method
# bind pat
pat = pat.bind self unless pat.is_a? Method
# call the new method
def_block.(old, pat, *args, **opts, &block)
end
# set visibility
mod.__send__ visibility, method_name
end
end
end
|
The final source code can be found here.
The reason why I do not use Module#refine
and Module#using
is that they currently have too much limitations and even bugs. I have already found as many as two bugs (#16107 and #16617). Although both of them have been fixed (or will be fixed), I cannot be sure that there will not be further and fatal bugs. I do not think these features are very reliable in recent Ruby versions.