Reputation: 1041
There is a pattern that I find myself to be frequently using, so I'd like to dry it up. I have stuff like this:
class InfoGatherer
def foo
true
end
def people
unless @people
@people = # Long and complex calculation (using foo)
end
@people
end
end
I'd like to dry this up to look like this:
class InfoGatherer
extend AttrCalculator
def foo
true
end
attr_calculator(:people) { # Long and complex calculation (using foo) }
end
To accomplish this, I defined a module AttrCalculator
to extend into InfoGatherer
. Here's what I tried:
module AttrCalculator
def attr_calculator(variable_name_symbol)
variable_name = "@#{variable_name_symbol}"
define_method variable_name_symbol do
unless instance_variable_defined?(variable_name)
instance_variable_set(variable_name, block.call)
end
instance_variable_get(variable_name)
end
end
end
Unfortunately, when I try something as simple as InfoGatherer.new.people
, I get:
NameError: undefined local variable or method `foo' for InfoGatherer:Class
Well, that's odd. Why is block
running in the scope of InfoGatherer:Class
, rather than its instance InfoGatherer.new
?
I know I can't use yield
, because that would try to catch the wrong block, as seen here.
I attempted to use self.instance_exec(block)
in the place of block.call
above, but then I received a new error:
LocalJumpError: no block given
Huh? I see the same error in this SO question, but I'm already using bracket notation, so the answers there don't seem to apply.
I also tried to use class_eval
, but I'm not sure how to call block
inside of a string. This certainly doesn't work:
class_eval("
def #{variable_name_symbol}
unless #{variable_name}
#{variable_name} = #{block.call}
end
#{variable_name}
end
")
Upvotes: 2
Views: 253
Reputation: 6085
To expand on the last persons
def people(varariable = nil)
@people ||= ComplexCalculation.new(variable).evaluate
end
class ComplexCalculation
def initialize(variable)
@variable = variable
end
def evaluate(variable)
#stuff
end
end
By extracting this class you are isolating that complexity and will have a much better experience.
Upvotes: 1
Reputation: 1041
The problem was that, inside the define_method
, self
was surprisingly InfoGatherer
, rather than an instance of InfoGatherer
. So I was on the right track with self.instance_exec(block)
.
The working solution is self.instance_exec(&block)
(note the ampersand). I guess the interpreter doesn't recognize that block
is a block unless you label it as such? If anyone can explain this better than me, please do.
As a side note, this is not the best way to solve this particular problem. See @sawa's answer for a clean way to memoize complicated calculations.
Upvotes: 1
Reputation: 168121
That use case is called memoization. It can be done easily like:
def people
@people ||= # Long and complex calculation (using foo)
end
You shouldn't go into the mess like you are.
Upvotes: 4