Ruby: argumentos de las palabras clave: ¿puedes tratar todos los argumentos de palabras clave como un hash? ¿Cómo?
arguments keyword (4)
¡Por supuesto! Solo usa el operador double splat ( **
).
def print_all(**keyword_arguments)
puts keyword_arguments
end
def mixed_signature(some: ''option'', **rest)
puts some
puts rest
end
print_all example: ''double splat (**)'', arbitrary: ''keyword arguments''
# {:example=>"double splat (**)", :arbitrary=>"keyword arguments"}
mixed_signature another: ''option''
# option
# {:another=>"option"}
Funciona igual que el splat normal ( *
), que se usa para recolectar parámetros. Incluso puede reenviar los argumentos de la palabra clave a otro método.
def forward_all(*arguments, **keyword_arguments, &block)
SomeOtherObject.some_other_method *arguments,
**keyword_arguments,
&block
end
Tengo un método que se ve así:
def method(:name => nil, :color => nil, shoe_size => nil)
SomeOtherObject.some_other_method(THE HASH THAT THOSE KEYWORD ARGUMENTS WOULD MAKE)
end
Para cualquier llamada dada, puedo aceptar cualquier combinación de valores opcionales. Me gustan los argumentos nombrados, porque puedo mirar la firma del método para ver qué opciones están disponibles.
Lo que no sé es si hay un atajo para lo que he descrito en letras mayúsculas en el ejemplo de código anterior.
En los viejos tiempos, solía ser:
def method(opts)
SomeOtherObject.some_other_method(opts)
end
Elegante, simple, casi infiel.
¿Hay un atajo para esos argumentos clave o tengo que reconstituir mi hash de opciones en la llamada al método?
¿Qué tal la sintaxis a continuación?
Para que funcione, trate los params
como una palabra clave reservada en su método y coloque esta línea en la parte superior del método.
def method(:name => nil, :color => nil, shoe_size => nil)
params = params(binding)
# params now contains the hash you''re looking for
end
class Object
def params(parent_binding)
params = parent_binding.local_variables.reject { |s| s.to_s.start_with?(''_'') || s == :params }.map(&:to_sym)
return params.map { |p| [ p, parent_binding.local_variable_get(p) ] }.to_h
end
end
Me divertí un poco con esto, así que gracias por eso. Esto es lo que se me ocurrió:
describe "Argument Extraction Experiment" do
let(:experiment_class) do
Class.new do
def method_with_mixed_args(one, two = 2, three:, four: 4)
extract_args(binding)
end
def method_with_named_args(one:, two: 2, three: 3)
extract_named_args(binding)
end
def method_with_unnamed_args(one, two = 2, three = 3)
extract_unnamed_args(binding)
end
private
def extract_args(env, depth = 1)
caller_param_names = method(caller_locations(depth).first.label).parameters
caller_param_names.map do |(arg_type,arg_name)|
{ name: arg_name, value: eval(arg_name.to_s, env), type: arg_type }
end
end
def extract_named_args(env)
extract_args(env, 2).select {|arg| [:key, :keyreq].include?(arg[:type]) }
end
def extract_unnamed_args(env)
extract_args(env, 2).select {|arg| [:opt, :req].include?(arg[:type]) }
end
end
end
describe "#method_with_mixed_args" do
subject { experiment_class.new.method_with_mixed_args("uno", three: 3) }
it "should return a list of the args with values and types" do
expect(subject).to eq([
{ name: :one, value: "uno", type: :req },
{ name: :two, value: 2, type: :opt },
{ name: :three, value: 3, type: :keyreq },
{ name: :four, value: 4, type: :key }
])
end
end
describe "#method_with_named_args" do
subject { experiment_class.new.method_with_named_args(one: "one", two: 4) }
it "should return a list of the args with values and types" do
expect(subject).to eq([
{ name: :one, value: "one", type: :keyreq },
{ name: :two, value: 4, type: :key },
{ name: :three, value: 3, type: :key }
])
end
end
describe "#method_with_unnamed_args" do
subject { experiment_class.new.method_with_unnamed_args(2, 4, 6) }
it "should return a list of the args with values and types" do
expect(subject).to eq([
{ name: :one, value: 2, type: :req },
{ name: :two, value: 4, type: :opt },
{ name: :three, value: 6, type: :opt }
])
end
end
end
Elegí devolver una matriz, pero podría modificar esto fácilmente para devolver un hash en su lugar (por ejemplo, al no preocuparse por el tipo de argumento después de la detección inicial).
Sí, esto es posible, pero no es muy elegante.
Deberá usar el método de parameters
, que devuelve una matriz de los parámetros del método y sus tipos (en este caso, solo tenemos argumentos de palabra clave).
def foo(one: 1, two: 2, three: 3)
method(__method__).parameters
end
#=> [[:key, :one], [:key, :two], [:key, :three]]
Sabiendo eso, hay varias formas de usar esa matriz para obtener un hash de todos los parámetros y sus valores proporcionados.
def foo(one: 1, two: 2, three: 3)
params = method(__method__).parameters.map(&:last)
opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}
Entonces tu ejemplo se vería
def method(name: nil, color: nil, shoe_size: nil)
opts = method(__method__).parameters.map(&:last).map { |p| [p, eval(p.to_s)] }.to_h
SomeOtherObject.some_other_method(opts)
end
Piense con cuidado acerca de usar esto. Es inteligente pero a costa de la legibilidad, a otros que leen tu código no les gustará.
Puedes hacerlo un poco más legible con un método de ayuda.
def params # Returns the parameters of the caller method.
caller_method = caller_locations(length=1).first.label
method(caller_method).parameters
end
def method(name: nil, color: nil, shoe_size: nil)
opts = params.map { |p| [p, eval(p.to_s)] }.to_h
SomeOtherObject.some_other_method(opts)
end
Actualización: Ruby 2.2 introdujo Binding#local_variables
que se puede usar en lugar de los Method#parameters
del Method#parameters
. Tenga cuidado porque debe llamar a local_variables
antes de definir cualquier variable local adicional dentro del método.
# Using Method#parameters
def foo(one: 1, two: 2, three: 3)
params = method(__method__).parameters.map(&:last)
opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}
# Using Binding#local_variables (Ruby 2.2+)
def bar(one: 1, two: 2, three: 3)
binding.local_variables.params.map { |p|
[p, binding.local_variable_get(p)]
}.to_h
end
#=> {:one=>1, :two=>2, :three=>3}