En utilisant procs avec les DSLs de Ruby
-
16-10-2019 - |
Question
Pour plus de commodité d'utilisateur et un code plus propre, je voudrais écrire une classe qui peut être utilisé comme ceci:
Encoder::Theora.encode do
infile = "path/to/infile"
outfile = "path/to/outfile"
passes = 2
# ... more params
end
Le défi est maintenant, d'avoir que les paramètres disponibles dans ma méthode encode.
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
Cette approche ne fonctionne pas. Lorsque le Proc est appelé, les variables ne sont pas évaluées dans le contexte de la classe Theora. En général, je voudrais utiliser method_missing pour mettre tous les paramètres dans une variable de classe de classe Theora, mais je ne trouve pas la bonne voie pour une entrée.
point Quelqu'un peut-moi dans la bonne direction?
La solution
Il ne peut se faire de la façon dont vous l'avez écrit, autant que je sache. Le corps du proc a sa propre portée, et les variables qui sont créées dans ce cadre ne sont pas visibles à l'extérieur il.
L'approche idiomatiques est de créer un objet de configuration et de passer dans le bloc, qui décrit le travail à faire en utilisant des méthodes ou des attributs de cet objet. Ensuite, ces paramètres sont lus lors de faire le travail. Telle est l'approche adoptée par create_table
des migrations ActiveRecord, par exemple.
Vous pouvez faire quelque chose comme ceci:
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
Autres conseils
Je ne suis pas sûr qu'il est possible d'obtenir le DSL à l'affectation d'utilisation, je pense que l'interprète Ruby assumera toujours infile
dans infile = 'path/to/something'
est une variable locale dans ce contexte (mais self.infile = 'path/to/something'
peut être au travail). Cependant, si vous pouvez vivre sans ce détail, vous pouvez mettre en œuvre votre DSL comme ceci:
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
et l'utiliser comme ceci:
Encoder::Theora.encode do
infile 'path/somewhere'
end
(mettre en œuvre les autres propriétés Similairement).
En jouant avec ce je suis arrivé à ce qui suit, que je ne recommande pas nécessairement, et qui ne correspond pas tout à fait la syntaxe requise, mais qui ne vous permet d'utiliser l'affectation (en quelque sorte). Alors peruse dans l'esprit d'exhaustivité:
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
Je crois que Binding.eval ne fonctionne que dans Ruby 1.9. Aussi, il semble les variables locales doivent être déclarées avant de céder ou il ne fonctionnera pas - quelqu'un sait pourquoi
OK, je dois d'abord dire que la réponse de pmdboi est très élégante et presque certainement le droit.
Pourtant, juste au cas où vous voulez une super DSL coupée vers le bas comme
Encoder::Theora.encode do
infile "path/to/infile"
outfile "path/to/outfile"
passes 2
end
Vous pouvez faire quelque chose de laid comme ceci:
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