Question

This question pretty much sums up the simple case for dynamically extending the class hierarchy in Ruby.

The problem I'm having is that I want to define this subclass with a DSL, and I think I'm a victim of my own complicated scope.

I have working code which uses a base class:

module Command
  class Command
    ...
  end
end

And then each command is implemented as a subclass:

module Command
  class Command_quit < Command
    def initialize
      name = "quit"
      exec do
        @user.client.should_terminate = true
      end
    end
  end
end

There is a lot of rote and repetition here, and I have envisioned a DSL which cleans this up significantly:

module Command
  define :quit do
    exec do # this is global.rb:7 from the error below
      @user.client.should_terminate = true
    end
  end
end

As you can see, I want to DRY out the boilerplate as I am only concerned with the contents of #initialize, which sets some metadata (such as name) and defines the exec block (which is the important part).

I have gotten stuck with the following module method:

module Command
  def self.define(cmd_name, &init_block)
    class_name = "Command_#{cmd_name.to_s}"
    class_definition = Class.new(Command)
    class_initializer = Proc.new do
      name = cmd_name
      init_block.call
    end
    ::Command.const_set class_name, class_definition
    ::Command.const_get(class_name).send(:define_method, :initialize, class_initializer)
  end
end

This code yields lib/commands/global.rb:7:in 'exec': wrong number of arguments (0 for 1+) (ArgumentError)

And suppose I have some metadata (foo) which I want to set in my DSL:

module Command
  define :quit do
    foo "bar" # this becomes global.rb:7
    exec do
      @user.client.should_terminate = true
    end
  end
end

I see lib/commands/global.rb:7:in block in <module:Command>': undefined method 'foo' for Command:Module (NoMethodError)

I think I've got my Proc/block/lambda-fu wrong here, but I'm struggling to get to the bottom of the confusion. How should I write Command::define to get the desired result? It seems like although Ruby creates Command::Command_help as a subclass of Command::Command, it's not actually inheriting any of the properties.

Était-ce utile?

La solution

When you refer to something in Ruby, it first look up something in local bindings, if it fails, it then look up self.something. self represents a context of the evaluation, and this context changes on class definition class C; self; end, method definition class C; def m; self; end; end, however, it won't change on block definition. The block captures the current self at the point of block definition.

module Command
  define :quit do
    foo "bar"     # self is Command, calls Command.foo by default
  end
end

If you want to modify the self context inside a block, you can use BasicObject.instance_eval (or instance_exec, class_eval, class_exec).

For your example, the block passed to define should be evaluated under the self context of an instance of the concrete command.

Here is an example. I added some mock method definition in class Command::Command:

module Command
  class Command
    # remove this accessor if you want to make `name` readonly
    attr_accessor :name

    def exec(&block)
      @exec = block
    end

    def foo(msg)
      puts "FOO => #{msg}"
    end

    def run
      @exec.call if @exec
    end
  end

  def self.define(name, &block)
    klass = Class.new(Command) do
      define_method(:initialize) do
        method(:name=).call(name)   # it would be better to make it readonly
        instance_eval(&block)
      end
      # readonly
      # define_method(:name) { name }
    end

    ::Command.const_set("Command_#{name}", klass)
  end

  define :quit do
    foo "bar"
    exec do
      puts "EXEC => #{name}"
    end
  end
end

quit = Command::Command_quit.new   #=> FOO => bar
quit.run                           #=> EXEC => quit
puts quit.class                    #=> Command::Command_quit

Autres conseils

Your problem is that blocks preserve the value of self (among other things) - when you call init_block.call and execution jumps to the block passed to define, self is the module Command and not the instance of Command_quit

You should be ok if you change your initialize method to

class_initializer = Proc.new do
  self.name = cmd_name # I assume you didn't just want to set a local variable
  instance_eval(&init_block)
end

instance_eval executes the block, but with the receiver (in this case your instance of Command_quit as the subclass.

An exception to the "blocks preserve self" behaviour is define_method: in that case self will always be object on which the method is called, much like with a normal method.

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top