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" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.