MetaRuby - Building Classes Dynamically

Last time, we created our own version of attr_reader using define_method. Today we will learn how to dynamically build classes in Ruby.

Keywords

Ruby defines a number of keywords that have special meaning. You probably use them so often, you don't even think about them, but many of them have programatic equivalents:

Keyword Ruby Method
class Class.new
module Module.new
def define_method
alias alias_method

We can use these to do some pretty neat things. Let's define a Class in a Module with an aliased method, and then see how we would do that programmatically without using Ruby's keywords:

module Example
  class Monkey
    def initialize(name)
      @name = name
    end

    def greet
      "Hi I'm #{@name}"
    end

    alias introduce greet 
  end
end

monkey = Example::Monkey.new("Hubert")
monkey.introduce #=> "Hi I'm Hubert"

Now let's do the same thing, but replace the keywords with Ruby methods:

Example = Module.new
Example::Monkey = Class.new do
  define_method :initialize do |name|
    @name = name
  end

  define_method :greet do
    "Hi I'm #{@name}"
  end

  alias_method :introduce, :greet 
end

monkey = Example::Monkey.new("Hubert")
monkey.introduce #=> "Hi I'm Hubert"

There are a few minor differences, for one, the modules and classes are anonymous, hence the Example = Module.new. The second obvious difference is that we use blocks for the module, class, and method bodies. Otherwise this is quite similar to the previous example.

Why might we want to do this? One common use case is to generate classes dynamically based on some sort of external schema.

A Class Factory

To illustrate this, let's make a method that will generate classes to decode binary data based on simple, declarative format. We'll use what we saw above, and leverage String#unpack, a method that converts a String of bytes into an Array of Ruby objects.

def data_class(fields)
  names = fields.keys
  pattern = fields.values.join

  Class.new do
    attr_reader :values

    define_method :initialize do |byte_string|
      @values = byte_string.unpack(pattern)
    end

    names.each_with_index do |name, i|
      define_method name do
        values[i]
      end
    end
  end

end

RGBColorData = data_class(red: "C", green: "C", blue: "C")

binary = [255, 128, 192].pack("CCC")
color = RGBColorData.new(binary)
color.red   #=> 255
color.green #=> 128
color.blue  #=> 192

We just wrote a method that let us read a color from binary data, now let's figure out how it works.

The data_class method takes a Hash of fields. The keys are names and the values define the pattern Ruby will use to unpack the data.

Next we define a new Class. Follow the indentation, and you'll see that the Class definition is the last expression in this method, so that's what will be returned.

Calling define_method :initialize creates a typical initialization method, but there's a slight twist. Notice that we reference pattern from outside the method definition. Since define_method is just a method, and its body is just a block, the methods you define this way can reference values outside the block.

Next we define an accessor method for each of the field names passed in.

Pack / Unpack

What about those calls to pack and unpack? pack will convert an Array of values into a binary representation based on a pattern. pack("CCC") generates the example binary data that RGBColorData consumes. In data_class the hash keys, ['C', 'C', 'C'], are joined into a single String, "CCC". That pattern is used to parse RGBColorData's input, which results in an Array of values, such as [255,128,192]. The dynamically defined accessor methods get an index from each_with_index, which lets each named field pluck its value out of the Array.

Recap

Ruby doesn't just let you define methods programmatically, you can define whole classes.

I hope this has piqued your curiosity a bit. Have you used Ruby to generate Classes before, or seen some particularly good examples of this? Let me know.

blog comments powered by Disqus
Monkey Small Crow Small