Utilizzando proc con DSL di Ruby
-
16-10-2019 - |
Domanda
Per una maggiore comodità d'uso e codice più pulito Vorrei scrivere una classe che può essere utilizzato in questo modo:
Encoder::Theora.encode do
infile = "path/to/infile"
outfile = "path/to/outfile"
passes = 2
# ... more params
end
La sfida è ora, per avere quel parametri disponibili nel mio metodo di codifica.
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
Questo approccio non funziona. Quando il Proc si chiama, le variabili non sono valutati nel contesto della classe Theora. Di solito Vorrei utilizzare method_missing per mettere tutti i parametri in una variabile di classe di classe Theora, ma io non trovare la strada giusta per una voce.
Qualcuno mi può punto nella giusta direzione?
Soluzione
Non può essere fatto nel modo che hai scritto, per quanto ne so. Il corpo del proc ha il suo campo di applicazione, e le variabili che si creano all'interno di tale portata non sono al di fuori visibile esso.
L'approccio idiomatica consiste nel creare un oggetto di configurazione e passarlo nel blocco, che descrive il lavoro da svolgere con metodi o attributi di quell'oggetto. Poi quelle impostazioni vengono lette quando si fa il lavoro. Questo è l'approccio adottato dal create_table
nelle migrazioni ActiveRecord, per esempio.
Così si può fare qualcosa di simile:
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
Altri suggerimenti
Non sono sicuro che sia possibile ottenere il DSL per l'assegnazione uso, penso che l'interprete Ruby sempre per scontato che infile
in infile = 'path/to/something'
è una variabile locale in quel contesto (ma self.infile = 'path/to/something'
può essere fatto per il lavoro). Tuttavia, se si può vivere senza quel particolare dettaglio, è possibile implementare il DSL in questo modo:
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
e usarlo in questo modo:
Encoder::Theora.encode do
infile 'path/somewhere'
end
(attuare le altre proprietà similmente).
In giocando con questo sono arrivato alla seguente, che io non consiglio necessariamente, e che non del tutto adatta la sintassi richiesta, ma che non consentono di utilizzare l'assegnazione (o quasi). Così peruse nello spirito di completezza:
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
Credo Binding.eval funziona solo in Ruby 1.9. Inoltre, sembra che le variabili locali devono essere dichiarate prima di cedere o non funzionerà -? Qualcuno sa perché
OK, in primo luogo devo dire che la risposta del pmdboi è molto elegante e quasi certamente quella giusta.
Ancora, nel caso in cui si vuole un super DSL cut-down come
Encoder::Theora.encode do
infile "path/to/infile"
outfile "path/to/outfile"
passes 2
end
Si può fare qualcosa di brutto in questo modo:
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