Created
January 16, 2013 06:35
-
-
Save avdi/4545113 to your computer and use it in GitHub Desktop.
Playing around with selective method import in Ruby
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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" |
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.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.