Использование процедур с DSL Руби
Для удобства пользователя и более чистого кода я хотел бы написать класс, который можно использовать так:
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 ответа
Это не может быть сделано так, как вы написали это, 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
Я не уверен, что можно заставить 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
(реализовать другие свойства аналогично).
Поэкспериментируя с этим, я пришел к следующему, которое я не обязательно рекомендую, и которое не совсем соответствует требуемому синтаксису, но которое позволяет вам использовать присваивание (вроде). Так что читайте в духе полноты:
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. Кроме того, кажется, что локальные переменные должны быть объявлены перед выдачей, иначе это не сработает - кто-нибудь знает почему?
Хорошо, сначала я должен сказать, что ответ 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