3

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?

3 Answers 3

4

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
Sign up to request clarification or add additional context in comments.

Comments

1

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

Comments

0

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

1 Comment

This is wrong, and both other answers prove that in the context of the class act_id is perfectly visible.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.