Pirating Ruby Methods For Fun And Profit
With all the SOPA coverage lately I wanted to post about software piracy (yarrrr!). But this post isn’t about the kind of software piracy you’re used to hearing about – it’s about pirating object methods using Ruby.
Sometimes while programming you might want to do what I call pirating a method – where you augment an object by borrowing a method from another module or class. Method pirating is fairly common to see in JavaScript code, especially to augment array-like objects like arguments which don’t have some useful methods that Arrays have, ex: Array.slice. Rubyists however aren’t quite the scallywags that JavaScript programmers are and don’t tend to do much pirating, but there’s times in Ruby where method pirating can be useful.
Including only what you need, nothing you don’t
Suppose that we have an extremely simple LinkedList implementation:
class LinkedList
def add(value)
return @head = Node.new(value) if @head.nil?
current = @head
while (!current.next.nil?)
current = current.next
end
current.next = Node.new(value)
end
def each
current = @head
while (!current.nil?)
yield current
current = current.next
end
end
def print
each { |node| puts node.value }
end
private
class Node
attr_accessor :next
attr_reader :value
def initialize(value)
@value = value
end
# comparison operator
def <=>(node)
@value <=> node.value
end
end
end
Now imagine we’d like to add a map (collect) method onto our LinkedList. We could implement this ourselves, but the Enumerable module already has the exact method we want. There is no need to reinvent the wheel – so we should find a way to make use of it.
Option #1: Mixin Enumerable
We could easily add Enumerable’s map() method to our LinkedList implementation by simply including the Enumerable module into our class like so:
class LinkedList
include Enumerable
# rest of implementation
end
Since we already implemented an each() method, Enumerable’s map() methods will automatically work with our LinkedList class – nice! The downside to this approach is that a bunch of other methods we didn’t need came along for the ride like min(), max(), reduce() (inject) – in fact every instance method defined in Enunmerable is now a part of LinkedList.
Now in Enumerable’s case this isn’t a disaster – we only had to define one method (each) to make Enumerable work with LinkedList. But more complex mixins might require several helper methods to be defined to work with classes that include them. If we only want one or two methods from a module that don’t require extra helper methods – that could be a lot of extra work defining methods for functionality we don’t need.
Option #2: Pirating, Yarrr!
If you only want a few methods that a module provides, then a better option might be to pirate that method instead.
In my last post on writing DSL’s I showed how we could do this using instance_eval to change the execution context of a block. This seems like a reasonable approach to pirating methods as we could turn a regular module method into a block and then attempt to execute it in the context of the class stealing the method. Let’s give it a shot:
module Foo
def hi
puts @words
end
end
class Boo
def initialize
@words = "boo"
end
def hi
method = Foo.instance_method(:hi)
instance_eval &method
end
end
boo = Boo.new
boo.hi
# wrong argument type UnboundMethod (expected Proc) (TypeError)
Uh-oh, looks like we wound up with an UnboundMethod, which isn’t associated with an object and thus uncallable. That’s ok though since in this case we’d like to bind our new unbound hi() method to the the Boo class so Boo.hi functions properly.
There is one issue though, UnboundMethods can only be bound to the same object type that they were created from. Ex. since we created an unbound method from Foo, we can only bind that method to another instance of Foo. Boo does not use Foo as a mixin so we’re currently unable to bind an UnboundMethod from Foo to an instance of Boo.
Fortunately Ruby allows to cheat. We can create a temporary Boo object that extends Foo without altering any other Boo instances using Object.extend. This then allows us to create an UnboundMethod that can be bound to a Boo instance even though it doesn’t mixin the Foo module!
Then all we have to do is bind our stolen method to the self variable in Boo and call that method.
class Boo
def hi
# temp class
pirate = Boo.new.extend(Foo)
# change execution context to this instance of Boo
stolen_method = pirate.method(:hi).unbind().bind(self)
# use our stolen method, prints "boo"
stolen_method.bind(self).call
end
end
We can easily apply the same technique to our LinkedList implementation to pirate the map() function off of Enumerable. Notice how we can pass parameters to call just like a normal method:
class LinkedList
def map(&block)
pirate = LinkedList.new.extend(Enumerable)
stolen_method = pirate.method(:map).unbind.bind(self)
stolen_method.call(&block)
end
end
Generalizing
It would be quite tedious to have to pirate mulitple methods this way, but fortunately it’s relatively easy to abstract this technique into a module method that we can mixin and call inside our LinkedList class:
module MethodPirate
def pirates(the_module, method)
pirate_class = self.new.extend(the_module)
stolen_method = pirate_class.method(method).unbind
send :define_method, method do |*args, &block|
stolen_method.bind(self).call(*args, &block)
end
end
end
class LinkedList
include MethodPirate
pirates Enumerable, :map
pirates Enumerable, :inject
# rest of implementation
end
You do wan’t to be careful here since this implementation isn’t very performant, notice how we create a new object and closure everytime we call pirate(). So for production use you’ll most likely want to cache the the generated classes/unbound methods.
Wrapping Up
Hopefully you’ve learned something about how to pirate methods in Ruby, metaprogramming and how it pirating methods from modules might be useful in situations where you only want to mixin parts of a module (or group of modules). Questions, comments or ideas feel free to post.