Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Defining Class Methods Dynamically in Ruby

In Ruby 1.9.3 I need to create a few class instances which each have similar instance- and class-methods but which vary only by a few fixed parameters. The distinction of their class type is also important so I cannot simply use separate instances of the same class.

A simplified example looks like this.

module Animal
private
  def self.make_animal(name, legs, noise)
    klass = Class.new
    klass.const_set(:NUM_LEGS, legs)
    klass.class.send(:define_method, :scream) { noise.upcase + '!' }
    Animal.const_set(name, klass)
  end
  make_animal :Tiger, 4, 'roar'
  make_animal :Human, 2, 'derp'
end

This seems to work fine except that the variables used in the block which dynamically defines the "scream" method are bound at runtime of the "scream" method instead of runtime of the "make_animal" method.

Animal::Human::NUM_LEGS # => 2 -- ok
Animal::Tiger::NUM_LEGS # => 4 -- ok
Animal::Human.scream # => "DERP!" -- ok
Animal::Tiger.scream # => "DERP!" -- fail!

How can I modify the above code so that the Tiger screams "ROAR!"?

[Note] I really do need to maintain the goofy OO structure in the example for reasons that are too involved to describe here. I'm interested only in learning how to programmatically define class methods on dynamically defined classes with parameterized method implementations.

like image 846
maerics Avatar asked Dec 12 '12 19:12

maerics


2 Answers

klass.class is the same in both cases (Class): all classes are instances of Class. As a result you're defining scream and then redefining it.

What are often thought of as class methods in ruby are actually singleton methods (there's lots of stuff to read about eigenclasses etc if you are interested).

The

def some_object.foo
end

Construct creates singleton methods. Very often this will be inside a class definition, using self but you can do it on anything, for example if you do

x = 'dog'
def x.bark
  "Woof"
end

Then x.bark will return woof, but bark won't be defined on any other string.

Here your method needs to reference your noise variable, so you'll need to use define_singleton_method to define your method.

If you're still in ruby 1.8 you can't use define_singleton_method - you need to use the fact that singleton methods are methods on the eigenclass.

klass = Class.new
eigenclass = class << klass; self; end
eigenclass.send(:define_method, :scream){noise}

Is equivalent to using define_singleton_method

like image 123
Frederick Cheung Avatar answered Oct 11 '22 19:10

Frederick Cheung


The problem with your code is not wrong binding moment. It's that you are defining method on Class#class. And it is, surprise-surprise, Class, the one and only. So you are overwriting "roar" version with "derp" version.

Instead, you should define methods on those dynamic classes directly. Here's my take (it's using instance var for the noise, hope that's not a problem).

module Animal
private
  def self.make_animal(name, legs, noise)
    klass = Class.new
    klass.const_set(:NUM_LEGS, legs)
    klass.instance_variable_set(:@noise, noise)

    klass.instance_eval do |k|
      def scream
        @noise.upcase + '!'
      end
    end

    Animal.const_set(name, klass)
  end

  make_animal :Tiger, 4, 'roar'
  make_animal :Human, 2, 'derp'
end

Animal::Human::NUM_LEGS # => 2
Animal::Tiger::NUM_LEGS # => 4

Animal::Human.scream # => "DERP!"
Animal::Tiger.scream # => "ROAR!"
like image 45
Sergio Tulentsev Avatar answered Oct 11 '22 20:10

Sergio Tulentsev