• Download Gist
a_ruby_metaprogramming_puzzle_for_polite_programmers.rb
Ruby
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 41 42 43 44 45 46 47 48 49
require 'test/unit/assertions'
include Test::Unit::Assertions
assert_equal "1.9.2", RUBY_VERSION
 
# A Ruby meta-programming puzzle for polite programmers.
 
# This puzzle was created by Matt Wynne (@mattwynne) on 2011-04-10 inspired by Jim Weirich's
# talk at the Scottish Ruby Conference 2011.
#
# The challenge is: you have a class Foo which you want to monkey-patch, but politely.
 
# Here's the default behaviour of Foo
class Foo
def bar
"bar"
end
end
 
assert_equal "bar", Foo.new.bar
 
module Baz
def bar
"wee" + super
end
end
 
# # Unfortunately, if we try to monkey-patch Foo like this, it won't work
# class Foo
# include Baz
# end
#
# # this fails:
# assert_equal "weebar", Foo.new.bar
#
# This is because the include trick inserts the module Baz higher into the inheritance tree
# meaning Foo's own implementation of bar is hit first.
 
# So let's write a helper method that can do the patching for us, by extending each instance
# of Foo as it is created.
#
def extend_every(type_to_patch, module_to_apply)
type_to_patch.define_singleton_method(:new) do |*args, &block|
super(*args, &block).extend(module_to_apply)
end
end
 
extend_every(Foo, Baz)
 
assert_equal "weebar", Foo.new.bar

Interesting, I didn't know about that method.

Would this still work if you wanted to extend_every Foo with mutliple modules? Would they all be added, or just the last one?

The above solution will only apply the last one, but we could change the implementation into this for supporting multi levels:

require 'test/unit/assertions'
include Test::Unit::Assertions
assert_equal "1.9.2", RUBY_VERSION

# A Ruby meta-programming puzzle for polite programmers.

# This puzzle was created by Matt Wynne (@mattwynne) on 2011-04-10 inspired by Jim Weirich's
# talk at the Scottish Ruby Conference 2011.
#
# The challenge is: you have a class Foo which you want to monkey-patch, but politely.

# Here's the default behaviour of Foo
class Foo
  def bar
    "bar"
  end
end

assert_equal "bar", Foo.new.bar

module Baz
  def bar
    "wee" + super
  end
end

module Buzz
  def bar
    "buzz" + super
  end
end

# # Unfortunately, if we try to monkey-patch Foo like this, it won't work
# class Foo
#   include Baz
# end
#
# # this fails:
# assert_equal "weebar", Foo.new.bar
# 
# This is because the include trick inserts the module Baz higher into the inheritance tree
# meaning Foo's own implementation of bar is hit first.

# So let's write a helper method that can do the patching for us, by extending each instance
# of Foo as it is created.
#
def extend_every(type_to_patch, module_to_apply)
  type_to_patch.instance_eval do
    @__modules_to_apply ||= []
    @__modules_to_apply << module_to_apply

    def new(*args, &block)
      obj = super(*args, &block)
      @__modules_to_apply.each { |m| obj.extend(m) }

      obj
    end
  end
end

extend_every(Foo, Baz)
extend_every(Foo, Buzz)

assert_equal "buzzweebar", Foo.new.bar

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.