Reputation: 17160
I was reading an article on meta-programming and it showed that you can define a method within another method. This is something that I had known for a while, but it made me ask myself the question: does this have any practical application? Is there any real life uses of defining a method within a method?
Ex:
def outer_method
def inner_method
# ...
end
# ...
end
Upvotes: 13
Views: 7836
Reputation: 14264
Not using def
. There is no practical application for that, and the compiler should probably raise an error.
There are reasons to define a method dynamically during the course of executing another method. Consider attr_reader
, which is implemented in C, but could be equivalently implemented in Ruby as:
class Module
def attr_reader(name)
define_method(name) do
instance_variable_get("@#{name}")
end
end
end
Here, we use #define_method
to define the method. #define_method
is an actual method; def
is not. That gives us two important properties. First, it takes an argument, which allows us to pass it the name
variable to name the method. Second, it takes a block, which closes over our variable name
allowing us to use it from inside the method definition.
So what happens if we use def
instead?
class Module
def attr_reader(name)
def name
instance_variable_get("@#{name}")
end
end
end
This doesn't work at all. First, the def
keyword is followed by a literal name, not an expression. That means that we're defining a method named, literally, #name
, which isn't what we wanted at all. Second, the body of the method refers to a local variable called name
, but Ruby won't recognize it as the same variable as the argument to #attr_reader
. The def
construct doesn't use a block, so it doesn't close over the variable name
anymore.
The def
construct doesn't allow you to "pass in" any information to parameterize the definition of the method you're defining. That makes it useless in a dynamic context. There's no reason to define a method using def
from within a method. You could always move the same inner def
construct out of the outer def
and end up with the same method.
Additionally, defining methods dynamically has a cost. Ruby caches the memory locations of methods, which improves performance. When you add or remove a method from a class, Ruby has to throw out that cache. (Before Ruby 2.1, that cache was global. As of 2.1, the cache is per-class.)
If you define a method inside another method, each time the outer method is called, it invalidates the cache. That's fine for top-level macros like attr_reader
and Rails' belongs_to
, because those are all called at when the program is starting up and then (hopefully) never again. Defining methods during the ongoing execution of your program will slow you down quite a bit.
Upvotes: 4
Reputation: 3072
I think there is another benefit to using inner methods which is about clarity. Think of it: a class with list of methods is a flat, non-structured list of methods. If you care about separation of concerns and keeping stuff in the same level of abstraction AND the piece of code is used only in one place, inner methods come to help while strongly hinting that they are used only in enclosing method.
Assume you have this method in a class:
class Scoring
# other code
def score(dice)
same, rest = split_dice(dice)
set_score = if same.empty?
0
else
die = same.keys.first
case die
when 1
1000
else
100 * die
end
end
set_score + rest.map { |die, count| count * single_die_score(die) }.sum
end
# other code
end
Now, that's sort of simple data structure transformation and more higher-level code, adding score of dice forming a set and those which do not belong to the set. But not very clear what's going on. Let's make it more descriptive. A simple refactoring follows:
class Scoring
# other methods...
def score(dice)
same, rest = split_dice(dice)
set_score = same.empty? ? 0 : get_set_score(same)
set_score + get_rest_score(rest)
end
def get_set_score(dice)
die = dice.keys.first
case die
when 1
1000
else
100 * die
end
end
def get_rest_score(dice)
dice.map { |die, count| count * single_die_score(die) }.sum
end
# other code...
end
Idea of get_set_score() and get_rest_score() is to document by using a descriptive (though not very good in this concocted example) what those pieces do. But if you have lots of methods like this, code in score() is not that easy to follow, and if you refactor either of the methods you might need to want to check what other methods use them (even if they are private - other methods of the same class could use them).
Instead, I'm starting to prefer this:
class Scoring
# other code
def score(dice)
def get_set_score(dice)
die = dice.keys.first
case die
when 1
1000
else
100 * die
end
end
def get_rest_score(dice)
dice.map { |die, count| count * single_die_score(die) }.sum
end
same, rest = split_dice(dice)
set_score = same.empty? ? 0 : get_set_score(same)
set_score + get_rest_score(rest)
end
# other code
end
Here, it should be more obvious that get_rest_score() and get_set_score() are wrapped into methods to keep logic of score() itself in the same abstraction level, no meddling with hashes etc.
Note that technically you can call Scoring#get_set_score and Scoring#get_rest_score, but in this case it would be bad style IMO, because semantically they are just private methods for the single method score()
So, having this structure you are always able to read the whole implementation of score() without looking any other method defined outside Scoring#score. Even though I don't see such Ruby code often, I think I'm going to convert more into this structured style with inner methods.
NOTE: Another option that doesn't look as clean but avoids the issue of name clashes is to simply use lambdas, which has been around in Ruby from the get go. Using the example, it would turn into
get_rest_score = -> (dice) do
dice.map { |die, count| count * single_die_score(die) }.sum
end
...
set_score + get_rest_score.call(rest)
It isn's as pretty -- someone looking at the code might wonder why all these lambdas, whereas using inner methods is pretty self-documenting. I'd still lean more towards just lambdas, as they don't have the issue of leaking potentially conflicting names to current scope.
Upvotes: 5
Reputation: 3937
I was thinking of a recursive situation, but I don't think it would make enough sense.
Upvotes: 0
Reputation: 15488
My favorite metaprogramming example like this is dynamically building a method that you're then going to use in a loop. For example, I have a query-engine I wrote in Ruby, and one of its operations is filtering. There are a bunch of different forms of filters (substring, equals, <=, >=, intersections, etc.). The naive approach is like this:
def process_filter(working_set,filter_type,filter_value)
working_set.select do |item|
case filter_spec
when "substring"
item.include?(filter_value)
when "equals"
item == filter_value
when "<="
item <= filter_value
...
end
end
end
But if your working sets can get large, you're doing this big case statement 1000s or 1000000s of times for each operation even though it's going to take the same branch on every iteration. In my case the logic is much more involved than just a case statement, so the overhead is even worse. Instead, you can do it like this:
def process_filter(working_set,filter_type,filter_value)
case filter_spec
when "substring"
def do_filter(item,filter_value)
item.include?(filter_value)
end
when "equals"
def do_filter(item,filter_value)
item == filter_value
end
when "<="
def do_filter(item,filter_value)
item <= filter_value
end
...
end
working_set.select {|item| do_filter(item,filter_value)}
end
Now the one-time branching is done once, up front, and the resulting single-purpose function is the one used in the inner loop.
In fact, my real example does three levels of this, as there are variations in the interpretation of both the working set and the filter value, not just the form of the actual test. So I build an item-prep function and a filter-value-prep function, and then build a do_filter function that uses those.
(And I actually use lambdas, not defs.)
Upvotes: 11
Reputation: 237010
Yes, there are. In fact, I'll bet you use at least one method that defines another method every day: attr_accessor
. If you use Rails, there are a ton more in constant use, such as belongs_to
and has_many
. It's also generally useful for AOP-style constructs.
Upvotes: 5