开发者

Using procs with Ruby's DSLs

For user convenience and more clean code I would like to write a class that can be used like this:

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

The challenge now is, to have that parameters available in my encode method.

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

This approach does not work. When the Proc is called, the variables are not evaluated in the context of the Theora class. Usually I would like to use method_missing to put every parameter into a class variable of class Theora, but I do not find the 开发者_如何学编程right way for an entry.

Can anyone point me into the right direction?


I'm not sure it's possible to get the DSL to use assignment, I think the Ruby interpreter will always assume that infile in infile = 'path/to/something' is a local variable in that context (but self.infile = 'path/to/something' can be made to work). However, if you can live without that particular detail, you can implement your DSL like this:

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

and use it like this:

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

(implement the other properties similarily).


It can't be done the way you've written it, AFAIK. The body of the proc has its own scope, and variables that are created within that scope are not visible outside it.

The idiomatic approach is to create a configuration object and pass it into the block, which describes the work to be done using methods or attributes of that object. Then those settings are read when doing the work. This is the approach taken by create_table in ActiveRecord migrations, for example.

So you can do something like this:

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


In playing around with this I arrived at the following, which I don't necessarily recommend, and which doesn't quite fit the required syntax, but which does allow you to use assignment (sort of). So peruse in the spirit of completeness:

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

I believe Binding.eval only works in Ruby 1.9. Also, it seems the local variables need to be declared before yielding or it won't work -- anyone know why?


OK, first I must say that pmdboi's answer is very elegant and almost certainly the right one.

Still, just in case you want a super cut-down DSL like

Encoder::Theora.encode do
  infile "path/to/infile"
  outfile "path/to/outfile"
  passes 2
end

You can do something ugly like this:

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
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜