Simon
Simon

Reputation: 1041

Pass block into define_method

The Problem

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?

The Research

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

Answers (3)

Austio
Austio

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

Simon
Simon

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

sawa
sawa

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

Related Questions