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
精彩评论