【问题标题】:Using procs with Ruby's DSLs在 Ruby 的 DSL 中使用 procs
【发布时间】:2011-01-21 08:06:34
【问题描述】:

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

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 类的类变量中,但我没有找到正确的输入方式。

谁能指出我正确的方向?

【问题讨论】:

  • 您可能要考虑更改问题的标题,它与 procs 没有太大关系,而与 DSLs 有很大关系。我建议也将proc 标签更改为dsl

标签: ruby metaprogramming dsl proc-object


【解决方案1】:

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

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

并像这样使用它:

Encoder::Theora.encode do
  infile 'path/somewhere'
end

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

【讨论】:

  • 很不确定它的美能否与 pmdboi 的方法相媲美。会玩它。谢谢。
  • 我同意,pmdboi 的解决方案是实现配置 DSL 的更常见方式。不过,有些像 Capistrano 和 Rails 3 的路线(以及其他路线)使用的样式与我的答案相似。
【解决方案2】:

不能按照你写的方式完成,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

【讨论】:

  • 产生配置会更加惯用,而且运行时不必将块打包到 proc 中。
  • 这听起来绝对像我一直在寻找的东西。干净,简单,不言自明。谢谢。
【解决方案3】:

在玩这个的过程中,我得到了以下内容,我不一定推荐它,并且不太符合所需的语法,但它确实允许您使用赋值(有点)。因此,本着完整的精神仔细阅读:

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。此外,似乎局部变量需要在 yield 之前声明,否则它将不起作用——有人知道为什么吗?

【讨论】:

  • 不错,但对我的 Theora 课程的用户来说并不明显。无论如何谢谢。
【解决方案4】:

好的,首先我必须说 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

【讨论】:

  • 我刚刚意识到我的答案与 Theo 的答案有多么相似。很抱歉之前没有意识到这一点。不同之处在于我依赖 Blockenspiel 而不是自己尝试做 instance_eval 的东西(我不喜欢)。
猜你喜欢
  • 2012-07-21
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-03-29
  • 2014-12-23
  • 1970-01-01
  • 2015-11-06
  • 2021-08-14
相关资源
最近更新 更多