Verwenden von Procs mit Rubys DSLs
-
16-10-2019 - |
Frage
Für Benutzerkontrolle und sauberer Code möchte ich eine Klasse schreiben, die so verwendet werden kann:
Encoder::Theora.encode do
infile = "path/to/infile"
outfile = "path/to/outfile"
passes = 2
# ... more params
end
Die Herausforderung besteht nun darin, diese Parameter in meiner Encode -Methode verfügbar zu haben.
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
Dieser Ansatz funktioniert nicht. Wenn der Proc aufgerufen wird, werden die Variablen im Kontext der Theora -Klasse nicht bewertet. Normalerweise möchte ich Method_miss verwenden, um jeden Parameter in eine Klassenvariable der Klassentheora zu setzen, aber ich finde nicht den richtigen Weg für einen Eintrag.
Kann mich jemand in die richtige Richtung weisen?
Lösung
Es kann nicht so gemacht werden, wie Sie es geschrieben haben, Afaik. Der Körper des Proc hat seinen eigenen Bereich, und Variablen, die innerhalb dieses Umfangs erzeugt werden, sind außerhalb dessen nicht sichtbar.
Der idiomatische Ansatz besteht darin, ein Konfigurationsobjekt zu erstellen und in den Block zu übergeben, der die Arbeiten mit Methoden oder Attributen dieses Objekts beschreibt. Dann werden diese Einstellungen bei der Arbeit gelesen. Dies ist der Ansatz von create_table
beispielsweise in ActivereCord -Migrationen.
So können Sie so etwas tun:
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
Andere Tipps
Ich bin mir nicht sicher, ob es möglich ist, die DSL dazu zu bringen, die Aufgabe zu verwenden. Ich denke, der Ruby -Dolmetscher wird das immer annehmen infile
in infile = 'path/to/something'
ist eine lokale Variable in diesem Kontext (aber self.infile = 'path/to/something'
kann zur Arbeit gemacht werden). Wenn Sie jedoch ohne dieses bestimmte Detail leben können, können Sie Ihre DSL wie folgt implementieren:
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
Und benutze es so:
Encoder::Theora.encode do
infile 'path/somewhere'
end
(Implementieren Sie die anderen Eigenschaften ähnlich).
Als ich damit herumspiele, kam ich nach Folgendes an, was ich nicht unbedingt empfehle und was nicht ganz zur erforderlichen Syntax passt, aber es ermöglicht, die Zuordnung (irgendwie) zu verwenden. So lesen Sie im Geiste der Vollständigkeit:
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
Ich glaube Binding.Val funktioniert nur in Ruby 1.9. Es scheint auch, dass die lokalen Variablen vor dem Nachgeben deklariert werden müssen, oder es wird nicht funktionieren - weiß jemand warum?
Ok, zuerst muss ich sagen, dass PMDBOIs Antwort sehr elegant und mit ziemlicher Sicherheit der richtige ist.
Trotzdem möchten Sie eine super geschnittene DSL mögen
Encoder::Theora.encode do
infile "path/to/infile"
outfile "path/to/outfile"
passes 2
end
Sie können so etwas Hässliches tun:
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