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.
