在Ruby的DSL上使用procs

时间:2011-01-21 08:06:35

标签: ruby metaprogramming dsl proc-object

为了方便用户和更干净的代码,我想编写一个可以像这样使用的类:

Encoder::Theora.encode do
  infile = "path/to/infile"
  outfile = "path/to/outfile"
  passes = 2
  # ... more params
end

现在的挑战是,在我的编码方法中提供这些参数。

module Encoder
  class Theora
    def self.encode(&proc)
      proc.call
      # do some fancy encoding stuff here
      # using the parameters from the proc
    end
  end
end

这种方法不起作用。调用Proc时,不会在Theora类的上下文中计算变量。通常我想使用method_missing将每个参数放入类Theora的类变量中,但是我没有找到正确的条目。

有人能指出我正确的方向吗?

4 个答案:

答案 0 :(得分:3)

我不确定是否可以让DSL使用赋值,我认为Ruby解释器总是假设infile中的infile = 'path/to/something'是该上下文中的局部变量(但{{1}可以使其工作)。但是,如果您可以在没有这些特定细节的情况下生活,您可以像这样实现您的DSL:

self.infile = 'path/to/something'

并像这样使用它:

module Encoder
  class Theora
    def self.encode(&block)
      instance = new
      instance.instance_eval(&block)
      instance
    end

    def infile(path=nil)
      @infile = path if path
      @infile
    end
  end
end

(类似地实现其他属性)。

答案 1 :(得分:1)

它不能像你写的那样完成,AFAIK。 proc的主体有自己的范围,在该范围内创建的变量在其外部不可见。

惯用法是创建配置对象并将其传递到块中,该块描述了使用该对象的方法或属性完成的工作。然后在完成工作时读取这些设置。这是create_table在ActiveRecord迁移中采用的方法,例如。

所以你可以这样做:

module Encoder
  class Theora
    Config = Struct.new(:infile, :outfile, :passes)

    def self.encode(&proc)
      config = Config.new
      proc.call(config)
      # use the config settings here
      fp = File.open(config.infile)       # for example
      # ...
    end
  end
end

# then use the method like this:
Encoder::Theora.encode do |config|
  config.infile = "path/to/infile"
  config.outfile = "path/to/outfile"
  config.passes = 2
  # ...
end

答案 2 :(得分:0)

在玩这个时我得到了以下内容,我不一定推荐,并且它不太符合所需的语法,但它确实允许你使用赋值(排序)。所以要完整地阅读:

module Encoder
  class Theora
    def self.encode(&proc)
      infile = nil
      outfile = nil
      yield binding
    end
  end
end

Encoder::Theora.encode do |b|
  b.eval <<-ruby
    infile = "path/to/infile"
    outfile = "path/to/outfile"
  ruby
end

我相信Binding.eval仅适用于Ruby 1.9。此外,似乎需要在屈服之前声明局部变量,否则它将无法工作 - 任何人都知道为什么?

答案 3 :(得分:0)

好的,首先我必须说pmdboi的答案非常优雅,几乎肯定是正确答案。

但是,如果你想要一个像

这样的超级缩减DSL
Encoder::Theora.encode do
  infile "path/to/infile"
  outfile "path/to/outfile"
  passes 2
end

你可以做这样丑陋的事情:

require 'blockenspiel'
module Encoder
  class Theora
    # this replaces pmdboi's elegant Struct
    class Config
      include Blockenspiel::DSL
      def method_missing(method_id, *args, &blk)
        if args.length == 1
          instance_variable_set :"@#{method_id}", args[0]
        else
          instance_variable_get :"@#{method_id}"
        end
      end
    end

    def self.encode(&blk)
      config = Config.new
      Blockenspiel.invoke blk, config
      # now you can do things like
      puts config.infile
      puts config.outfile
      puts config.passes
    end
  end
end