Skip to content

Instantly share code, notes, and snippets.

@avdi
Created January 16, 2013 06:35
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save avdi/4545113 to your computer and use it in GitHub Desktop.
Save avdi/4545113 to your computer and use it in GitHub Desktop.
Playing around with selective method import in Ruby
# 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
Copy link

henrik commented Jan 16, 2013

undef_method does still cover ancestor methods, but maybe remove_method would do the trick?

@avdi
Copy link
Author

avdi commented Jan 16, 2013

@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.

@avdi
Copy link
Author

avdi commented Jan 16, 2013

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.

@jballanc
Copy link

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