-
-
Save avdi/4545113 to your computer and use it in GitHub Desktop.
# Generate a module which imports a given subset of module methods | |
# into the including module or class. | |
def Methods(source_module, *method_names) | |
all_methods = source_module.instance_methods + | |
source_module.private_instance_methods | |
unwanted_methods = all_methods - method_names | |
import_module = source_module.clone | |
import_module.module_eval do | |
define_singleton_method(:to_s) do | |
"ImportedMethods(#{source_module}: #{method_names.join(', ')})" | |
end | |
private(*method_names) | |
remove_method(*unwanted_methods) | |
end | |
import_module | |
end | |
# In case you're wondering: no, it doesn't work to include the source | |
# module and then call undef_method on unwanted methods. Well, it | |
# works; but it *hides* any methods by the same name further up the | |
# ancestor chain. This pretty much defeats the purpose of selective | |
# import, since one reason for selective import is to keep a module | |
# from stomping on methods from earlier in the ancestor chain. | |
# Generate a module which imports a given subset of functions into the | |
# including module or class. These are "functions" because they don't | |
# execute in the context of self; they execute in the context of an | |
# anonymous object. In other words, the imported functions are | |
# forwarded. | |
def Functions(source_module, *method_names) | |
all_methods = source_module.instance_methods + | |
source_module.private_instance_methods | |
unwanted_methods = all_methods - method_names | |
object = Object.new.extend(source_module) | |
Module.new do | |
define_singleton_method(:to_s) do | |
"ImportedFunctions(#{source_module}: #{method_names.join(', ')})" | |
end | |
method_names.each do |name| | |
define_method(name) do |*args, &block| | |
object.send(name, *args, &block) | |
end | |
end | |
private(*method_names) | |
end | |
end | |
User = Struct.new(:name) | |
module Users | |
def merge(user1, user2) | |
names = user1.name.split.zip(user2.name.split).flatten | |
User.new(names.join(" ")) | |
end | |
def fight(user1, user2) | |
puts "#{[user1, user2].sample.name} wins" | |
end | |
def who_am_i | |
self | |
end | |
end | |
module HippyDippy | |
def fight | |
"Make love, not war!" | |
end | |
end | |
class ImportsMethods | |
include HippyDippy | |
include Methods(Users, :merge, :who_am_i) | |
public :who_am_i | |
def do_some_stuff | |
user1 = User.new("Zap Rowsdower") | |
user2 = User.new("Space Chief") | |
merge(user1, user2).name | |
end | |
end | |
class ImportsFunctions | |
include Functions(Users, :merge, :who_am_i) | |
public :who_am_i | |
def do_some_stuff | |
user1 = User.new("Zap Rowsdower") | |
user2 = User.new("Space Chief") | |
merge(user1, user2).name | |
end | |
end | |
ImportsMethods.included_modules | |
# => [ImportedMethods(Users: merge, who_am_i), | |
# HippyDippy, | |
# PP::ObjectMixin, | |
# Kernel] | |
ImportsFunctions.included_modules | |
# => [ImportedFunctions(Users: merge, who_am_i), PP::ObjectMixin, Kernel] | |
obj1 = ImportsMethods.new | |
obj2 = ImportsFunctions.new | |
obj1.public_methods.include?(:merge) # => false | |
obj1.private_methods.include?(:merge) # => true | |
obj1.do_some_stuff # => "Zap Space Rowsdower Chief" | |
obj2.do_some_stuff # => "Zap Space Rowsdower Chief" | |
# Non-imported methods are left alone | |
obj1.fight # => "Make love, not war!" | |
# illustrate the difference between importing functions and methods: | |
obj1.who_am_i # => #<ImportsMethods:0x000000009e9260> | |
obj2.who_am_i # => #<Object:0x00000000b24e18> | |
module Users | |
def merge(user1, user2) | |
# switcheroo! | |
names = user2.name.split.zip(user1.name.split).flatten | |
User.new(names.join(" ")) | |
end | |
end | |
# Unfortunately, changes to the original source module don't take | |
# effect on imported methods because of the module cloning | |
obj1.do_some_stuff # => "Zap Space Rowsdower Chief" | |
# Imported functions DO pick up on changes to the source module | |
obj2.do_some_stuff # => "Space Zap Chief Rowsdower" |
@henrik: nope, I tried that too! Because I don't want to modify the source module, I had to call it in the generated module - where it doesn't work, because you can only remove_method
in a class or module where that method is actually defined.
I also tried defining something I called ignore_method
, but that failed because I needed a supersuper
keyword instead of plain-old super
. It's a tricky problem.
Another potential problem with importing methods is that because the unwanted methods are actually removed from the cloned module, any imported methods which depend on other methods in the source module will break. All things considered, I think the include Functions(...)
approach is safer; albeit surprising to someone who expects it to act like ordinary module inclusion. It's also semantically closer to the idea of importing a "free function" into the local namespace for convenience' sake, as opposed to the idea of extending the current class - which is usually what I want anyway.
Very reminiscent of library forms in Scheme: http://www.r6rs.org/final/html/r6rs/r6rs-Z-H-10.html#node_sec_7.1 😉 I like it!
...but seriously, I'd totally back this as an official feature in a future version of Ruby.
undef_method does still cover ancestor methods, but maybe remove_method would do the trick?