Pregunta

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.

¿Fue útil?

Solución

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

Otros consejos

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.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top