linkyndy
linkyndy

Reputation: 17900

Dynamically create static variables in a class

I have the following class:

class A
  X = %w(a b c)
end

How can I create a list of static variables for class A using the values in X so that I will end up with the same thing as:

class A
  a = 1
  b = 2
  c = 3
end

I found out that methods can be defined with define_method, but I don't quite have a clue on how to do this for variables.

Upvotes: 1

Views: 442

Answers (3)

Patrik
Patrik

Reputation: 355

With some meta programming madness you could do something like

  class A
    class << self
      def A.X(args)
        puts "Creating methods"
        args.each do |arg|
          define_singleton_method(arg) {
            if class_variable_defined? "@@#{arg}"
              class_variable_get("@@#{arg}")
            else
             nil
            end
          }
          define_singleton_method("#{arg}=") { |val|
            class_variable_set("@@#{arg}", val)
          }
        end
      end
    end
  end

Would allow you to do like this

  class A
    X %w(a b c)
    A.a = 1
    A.b = 2
    A.c = 3
  end

Need to prefix with A.a, A.b and so on because of some ruby syntax thing

Upvotes: 1

J&#246;rg W Mittag
J&#246;rg W Mittag

Reputation: 369458

Unfortunately, what you want is AFAIK impossible. You simply cannot introduce local variables into a scope dynamically. You might be tempted to do something like this:

class A
  X = %w(a b c)
end

class A
  bnd = binding

  X.each.with_index(1) do |var, i|
    bnd.local_variable_set(var, i)
  end
end

But that won't work. When you use local_variable_set to create a new variable, that variable will only exist within the Binding object, not in the scope from which the Binding was created.

class A
  bnd = binding

  X.each.with_index(1) do |var, i|
    eval("#{var} = #{i}", bnd)
  end
end

This won't work either. In fact, it is simply equivalent to the first.

Okay, so you think: "let's not use a Binding, then, just plain eval":

class A
  X.each.with_index(1) do |var, i|
    eval("#{var} = #{i}")
  end
end

Nope, this doesn't work either. The local variables still only get created inside the eval, but they don't leak outside. (They did leak in 1.8 and older, but they don't anymore in 1.9+.) And in fact, even if they did get created, they would get created in the scope of the block. So, you would have to use for/in instead.

Also note that even if you managed to inject local variables into the scope, they are still local variables: you cannot access them from anywhere else. (Unless you somehow manage to get hold of the Binding and call local_variable_get.)

It would make much more sense to make them methods, which is much easier:

class A
  X.each.with_index(1) do |var, i|
    define_singleton_method(:var) do i end
  end
end

Now, they can also be accessed from outside the scope:

A.a # => 1

or using the somewhat obscure method calling syntax using the scope resolution operator:

A::a # => 1

Of course, since they always return the same value anyway, why not make them constants?

class A
  X.each.with_index(1) do |var, i|
    const_set(var.upcase, i)
  end
end

Upvotes: 4

Stefan
Stefan

Reputation: 114178

Constants are defined like this (they have to start with an uppercase letter):

class A
  A = 1
  B = 2
  C = 3
end

A::A #=> 1
A::B #=> 2
A::C #=> 3

To define them dynamically, use const_set:

class A
  %w(A B C).each.with_index(1) { |c, i| const_set(c, i) }
end

Upvotes: 3

Related Questions