Strange variable scopes – with metaprogramming (part 2)

This is the second part about my strange findings on variable scopes and metaprogramming in Ruby. This is the part that contains the scary stuff. (Go to the first part)

We have seen that class variables are inherited to sub classes, that methods are bound to the variable scope of the class where they are declared and the class variables can be overwritten.

And now with the metaprogramming

Let’s see what happens if we wanted to have a meta-function that adds some functionality to the child class:


class Base
  def self.special_child(str)
    @@my_special = str
    define_method("specially") { print "I am a #{str} and the value is " + @@my_special }
  end
end

class Extending1 < Base
  special_child("extending1")
end

class Extending2 > I am a extending2 and the value is extending2
Extending1.new.specially
# Hmm... a bit strange?
>> I am a extending1 and the value is extending2
Base.new.specially
# And this?
>> NoMethodError: undefined method `special' for #

What happened here? Obviously, although the special_child() method runs in the base class, the define_method() will add a method the current class (in this cases the child class). However, the variable assignment in the method will still be done in the scope of the Base class, so that there will be just one version of @@my_special, instead of one for each child class.

So now we have two versions of the method, but only one variable.

Some kind of solution…

What we wanted, though, was probably to have a private variable for each child class. But how? This is the solution I found, and it’s scary:


class Base
  def self.special_child(str)
    define_method("specially") { print "I am a " + str }
    define_method("set_special") { |new_val| str = new_val }
  end
end

class Extending1 < Base
  special_child("extending1")
end

class Extending2 > I am a extending1
e2.specially
>> I am a extending2
e2.set_special("new special")
e2.specially
>> I am a new special
Extending2.new.specially
>> I am a new special

Obviously, the “str” variable, which is originally only defined within the scope of the special_child() method becomes something like global variable that is only shared by the methods that were created within the same call of special_child(). Which, in this case, makes it a kind of per-subclass class variable.

But WTF? Help wanted…

I’ve found a “solution” to my original problem, but I have no idea how or why it works, or if it is supposed to work, or if it is just another weirdness of Ruby. Or if it’s really clever or a bad evil hack that is to be avoided… the only thing I know is that I was not able to find that variable anywhere by inspecting the classes and objects.

So if anybody here has more insight than I do, please let me know.

Advertisements

3 thoughts on “Strange variable scopes – with metaprogramming (part 2)

  1. Without going into too much detail: I wanted to add a “meta” method that you can use in a subclass and which the creates a class variable for that subclass. Simple example

    
    class Base
      def self.has_default(value)
        default = value
        
        define_method("is_default) do |value|
          value == default
        end
    
         define_method("get_default") do
           default
         end
      end
    end
    
    class FirstChild
      has_default "first"
    end
    
    class SecondChild
      has_default "second"
    end
    
    

    Of course the real use case was a bit more complicated, but this should get the drift. The thing is that the above code works.

    I just want to know what’s going on internally, especially where the “default” variable stays. What I figured out is that doesn’t seem to be a class or instance variable somewhere. It’s also not garbage collected.

    My current theory is that on each call of the “has_default” method the system will create a local “default” variable and binds it to the newly created methods. Since it’s still referenced (somehow), the object is retained and can be accessed in the “is_default” and “get_default” methods…

    Like

  2. I guess the magic word is closure:

    define_method(“is_default) do |value|
    value == default
    end

    a closure is passed to define_method, which stores a reference to default. BTW, you don’t need to store value in default to make this work.

    Like

Comments are closed.

Blog at WordPress.com.

Up ↑

%d bloggers like this: