Pattern Methods
April 9th, 2008 Filed Under ruby
Rails has a series of methods, find_by_*, find_all_by_*, and find_and_create_by_* where the * can be any database column, or any combination of database columns–in any order–joined by _and_. When I first started coding in Rails, I had trouble figuring out how they worked. Did you have to put the columns in a particular order? Did they have to define every single possible combination?
The fact that every combination worked is how I realized that they couldn’t be defining any combination–there were just too many possibilities. A bit more Rails experience let me figure out the solution–clever usage of method_missing. I had learned Ruby, but I never realized when reading about method_missing, just how powerful it was–it seemed like another one of those “cute but why?” things. This seems like a major shift, though. In most languages, a method is a function with a specific name. Single name, does a particular thing. But with method_missing, you can have an object respond to practically anything you want, regardless of whether you defined it or not.
So, there’s the wild and wonderful world of catching things with method_missing. But there’s slightly more narrow purpose that’s used for quite a lot of the method_missing calls–method names that match a regular expression. That’s how the find_by_* and similar methods work, they look to see if they match a particular regular expression.
This is a good solution. But there was always a nagging issue with these solutions for me. They didn’t look like, or work like, any other methods out there. You can’t test for them using respond_to?, you can’t see them in the list of methods using methods or instance_methods. Ruby’s powerful reflection features are of no use for them. Similarly, it’s a little awkward to implement. You need to override method_missing, making sure to keep your place in the chain. It feels more hidden than just implementing a method using def or define_method.
So I created a bit more direct of a solution. To implement a method whose name is anything that matches a regular expression (I’m calling them patterned methods), instead of calling define_method, you can call define_pattern_method, as in the below:
-
class MyClass
-
-
define_pattern_method /^al/ do |*args|
-
# Do something here
-
end
-
-
end
-
-
myObj = MyClass.new
-
-
myObj.respond_to? :alphabet
-
# => true
-
-
myObj.respond_to? :all_fall_down
-
# => true
-
-
myObj.respond_to? :avacado
-
# => false
Okay, excellent! But since there isn’t a single method name, how do you know what method was called? Why, use pattern_name, of course!
-
class MyClass
-
-
define_pattern_method /^al(.+)/ do |*args|
-
[ pattern_name, pattern_name(1) ]
-
end
-
-
end
-
-
myObj = MyClass.new
-
-
myObj.alphabet
-
# => [ "alphabet", "phabet" ]
-
-
myObj.all_fall_down
-
# => [ "all_fall_down", "l_fall_down" ]
Note that you can also get the groups from the match using pattern_name by passing in the index of the group.
Now, what about passing a block into the method? Since inside, define_pattern_method really uses define_method, which can’t take block methods? Well, I’ve got a solution around it. Not a perfect solution (a better one might be to use Ruby2Ruby and class_eval, but that would make this require Ruby2Ruby), but a pretty good one:
-
class MyClass
-
-
define_pattern_method /^al/ do |*args|
-
if pattern_block_given?
-
pattern_block_yield(*args)
-
end
-
end
-
-
end
-
-
myObj = MyClass.new
-
-
myObj.alphabet(1, 2, 3) do |a, b, c|
-
[c, b, a]
-
end
-
# => [3, 2, 1]
-
-
myObj.alphabet(1, 2, 3)
-
# => nil
And that’s where we are now. This is an initial attempt, so it’s not perfect. In particular, you can’t add them to classes or modules (I’ll need to watch the Ruby Internals talk from MountainWest again). And in terms of reflection, I’ve implemented respond_to?, Class#instance_methods and Object#methods to list the pattern methods, but hide the underlying methods.
Now for the fun! Here’s some toy examples using it. What kind of cool uses can you think of?
-
class MyClass
-
-
define_pattern_method /(.+)_and_(.+)/ do
-
send pattern_name(1)
-
send pattern_name(2)
-
end
-
-
define_pattern_method /(.+)_after_(.+)/ do
-
send pattern_name(2)
-
send pattern_name(1)
-
end
-
-
define_pattern_method /^(.+)_(\d+)_times/ do |*args|
-
inner_method = pattern_name 1
-
count = pattern_name(2).to_i
-
-
count.times { send(inner_method, *args) }
-
end
-
-
def foo
-
puts "foo called"
-
end
-
-
def bar
-
puts "bar called"
-
end
-
-
def quux
-
puts "quux called"
-
end
-
-
end
-
-
myObj.foo_after_bar_and_quux_after_foo
Prints:
bar called foo called quux called
-
myObj.foo_5_times_after_bar
Prints:
bar called foo called foo called foo called foo called foo called
And very similar to Dr. Nic’s map_by_method:
-
class Array
-
-
define_pattern_method /^map_by_([_a-zA-Z][_a-zA-Z0-9]+)$/ do |*args|
-
inner_methods = pattern_name(1).split(‘_then_’)
-
-
map do |x|
-
inner_methods.inject(x) { |obj, meth| obj.send(meth, *args) }
-
end
-
end
-
-
end
-
-
[1, -2, -3, 4].map_by_succ_then_abs
-
# => [2, 1, 2, 5]
Post Linx
Permalink | Trackback |
|
Print This Article |