user1934428
user1934428

Reputation: 22225

Ruby variable from outer scope undefined within a block - why?

This part of code dynamically creates several classes:

(1..MAX_ACT).each do |act_id|
  klass = Class.new(ActB) do
    def initialize(trg)
      super(trg, act_id)
    end
  end
  Object.const_set("Act#{act_id}", klass)
end

In this case, the common base class (ActB) has a constructor with two parameters, while the child classes have a constructor with one parameter.

Running this code works well, but when I later try to instantiate one of these classes, for example

Act3.new(4)

I get the error message

NameError: undefined local variable or method `act_id' for #<Act3:0x00000006008b7990>

The error message must refer to the line

super(trg, act_id)

because this is the only place in my program where I am using this variable. However, this variable is defined a few lines above, when it says

(1..MAX_ACT).each do |act_id|

I had expected, that the do...end block creates a closure for the constructor, where act_id is bound. However, this doesn't seem to be the case.

Why does my example not work? How do I have to do it correctly?

Upvotes: 3

Views: 254

Answers (3)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

Just out of curiosity, there is a hack, allowing to fool scoping and still use def initialize :)

class ActB
  def initialize(trg, act_id)
    puts "ActID: #{act_id}"
  end 
end
(1..MAX_ACT).each do |act_id|
  klass = Class.new(ActB) do
    @act_id = act_id
    def initialize(trg)
      super(trg, self.class.instance_variable_get(:@act_id))
    end 
  end 
  Object.const_set("Act#{act_id}", klass)
end

Act1.new :foo
#⇒ ActID: 1
Act2.new :foo
#⇒ ActID: 2

Upvotes: 1

matthewd
matthewd

Reputation: 4420

def (and class and module) creates a fresh local scope, which doesn't inherit any locals from outside.

So you're right that the Class.new do .. end creates a closure... but the inner def doesn't share it.

If you need standard block behaviour, you can use define_method instead:

(1..MAX_ACT).each do |act_id|
  klass = Class.new(ActB) do
    define_method :initialize do |trg|
      super(trg, act_id)
    end
  end
  Object.const_set("Act#{act_id}", klass)
end

Upvotes: 4

thesecretmaster
thesecretmaster

Reputation: 1984

The problem here is that the block passed to Class.new is executed in the context of that class. In the context of that class, act_id is not defined. So, to fix this, you can move the method definition outside of the class initialization, like so:

(1..MAX_ACT).each do |act_id|
  klass = Class.new(ActB)
  klass.define_method(:initialize) do |trg|
    super(trg, act_id)
  end
  Object.const_set("Act#{act_id}", klass)
end

Upvotes: 0

Related Questions