Parámetros con nombre en Ruby Structs
named-parameters (11)
Soy bastante nuevo para Ruby así que me disculpo si esta es una pregunta obvia.
Me gustaría usar parámetros con nombre al crear una instancia de Struct, es decir, ser capaz de especificar qué elementos de Struct obtienen qué valores, y el resto es nulo por defecto.
Por ejemplo, quiero hacer:
Movie = Struct.new :title, :length, :rating
m = Movie.new :title => ''Some Movie'', :rating => ''R''
Esto no funciona
Así que se me ocurrió lo siguiente:
class MyStruct < Struct
# Override the initialize to handle hashes of named parameters
def initialize *args
if (args.length == 1 and args.first.instance_of? Hash) then
args.first.each_pair do |k, v|
if members.include? k then
self[k] = v
end
end
else
super *args
end
end
end
Movie = MyStruct.new :title, :length, :rating
m = Movie.new :title => ''Some Movie'', :rating => ''R''
Esto parece funcionar bien, pero no estoy seguro de si hay una forma mejor de hacerlo, o si estoy haciendo algo bastante loco. Si alguien puede validar / desmantelar este enfoque, estaría muy agradecido.
ACTUALIZAR
Ejecuté esto inicialmente en 1.9.2 y funciona bien; sin embargo, después de haberlo probado en otras versiones de Ruby (gracias rvm), funciona / no funciona de la siguiente manera:
- 1.8.7: No funciona
- 1.9.1: Trabajando
- 1.9.2: Trabajando
- JRuby (configurado para ejecutarse como 1.9.2): no funciona
JRuby es un problema para mí, ya que me gustaría mantenerlo compatible con eso para el despliegue.
AÚN OTRA ACTUALIZACIÓN
En esta pregunta en constante aumento, experimenté con las diversas versiones de Ruby y descubrí que Structs en 1.9.x almacena sus miembros como símbolos, pero en 1.8.7 y JRuby, se almacenan como cadenas, así que actualicé el código para ser el siguiente (teniendo en cuenta las sugerencias ya amablemente dadas):
class MyStruct < Struct
# Override the initialize to handle hashes of named parameters
def initialize *args
return super unless (args.length == 1 and args.first.instance_of? Hash)
args.first.each_pair do |k, v|
self[k] = v if members.map {|x| x.intern}.include? k
end
end
end
Movie = MyStruct.new :title, :length, :rating
m = Movie.new :title => ''Some Movie'', :rating => ''R''
Esto ahora parece funcionar para todos los sabores de Ruby que he probado.
¿Has considerado OpenStruct?
require ''ostruct''
person = OpenStruct.new(:name => "John", :age => 20)
p person # #<OpenStruct name="John", age=20>
p person.name # "John"
p person.adress # nil
Basado en la respuesta de @Andrew Grimm, pero usando los argumentos de la palabra clave de Ruby 2.0:
class Struct
# allow keyword arguments for Structs
def initialize(*args, **kwargs)
param_hash = kwargs.any? ? kwargs : Hash[ members.zip(args) ]
param_hash.each { |k,v| self[k] = v }
end
end
Tenga en cuenta que esto no permite la mezcla de argumentos regulares y de palabra clave; solo puede usar uno u otro.
Entre menos sepas, mejor. No es necesario saber si la estructura de datos subyacente utiliza símbolos o cadenas, o incluso si puede abordarse como Hash
. Solo usa los setters de atributos:
class KwStruct < Struct.new(:qwer, :asdf, :zxcv)
def initialize *args
opts = args.last.is_a?(Hash) ? args.pop : Hash.new
super *args
opts.each_pair do |k, v|
self.send "#{k}=", v
end
end
end
Toma tanto argumentos posicionales como palabras clave:
> KwStruct.new "q", :zxcv => "z"
=> #<struct KwStruct qwer="q", asdf=nil, zxcv="z">
Para un equivalente de 1 a 1 con el comportamiento de Struct (aumentar cuando no se da el argumento requerido) lo uso a veces (Ruby 2+):
def Struct.keyed(*attribute_names)
Struct.new(*attribute_names) do
def initialize(**kwargs)
attr_values = attribute_names.map{|a| kwargs.fetch(a) }
super(*attr_values)
end
end
end
y de allí en adelante
class SimpleExecutor < Struct.keyed :foo, :bar
...
end
Esto generará un KeyError
si se perdió un argumento, tan agradable para constructores y constructores más estrictos con muchos argumentos, objetos de transferencia de datos y similares.
Podría reorganizar el if
s.
class MyStruct < Struct
# Override the initialize to handle hashes of named parameters
def initialize *args
# I think this is called a guard clause
# I suspect the *args is redundant but I''m not certain
return super *args unless (args.length == 1 and args.first.instance_of? Hash)
args.first.each_pair do |k, v|
# I can''t remember what having the conditional on the same line is called
self[k] = v if members.include? k
end
end
end
Si necesita mezclar argumentos regulares y de palabras clave, siempre puede construir el inicializador a mano ...
Movie = Struct.new(:title, :length, :rating) do
def initialize(title, length: 0, rating: ''PG13'')
self.title = title
self.length = length
self.rating = rating
end
end
m = Movie.new(''Star Wars'', length: ''too long'')
=> #<struct Movie title="Star Wars", length="too long", rating="PG13">
Esto tiene el título como un primer argumento obligatorio solo para ilustración. También tiene la ventaja de que puede establecer los valores predeterminados para cada argumento de palabra clave (¡aunque es poco probable que sea útil si se trata de películas!).
Si sus claves hash están en orden, puede llamar al operador splat para rescatarlas:
NavLink = Struct.new(:name, :url, :title)
link = {
name: '''',
url: ''https://.com'',
title: ''Sure whatever''
}
actual_link = NavLink.new(*link.values)
#<struct NavLink name="", url="https://.com", title="Sure whatever">
Sintetizar las respuestas existentes revela una opción mucho más simple para Ruby 2.0+:
class KeywordStruct < Struct
def initialize(**kwargs)
super(*members.map{|k| kwargs[k] })
end
end
El uso es idéntico al Struct
existente, donde cualquier argumento no proporcionado será predeterminado a nil
:
Pet = KeywordStruct.new(:animal, :name)
Pet.new(animal: "Horse", name: "Bucephalus") # => #<struct Pet animal="Horse", name="Bucephalus">
Pet.new(name: "Bob") # => #<struct Pet animal=nil, name="Bob">
Si desea requerir los argumentos como los kwargs necesarios de Ruby 2.1 +, es un cambio muy pequeño:
class RequiredKeywordStruct < Struct
def initialize(**kwargs)
super(*members.map{|k| kwargs.fetch(k) })
end
end
En ese punto, la initialize
anulación para dar ciertos valores predeterminados de kwargs también es factible:
Pet = RequiredKeywordStruct.new(:animal, :name) do
def initialize(animal: "Cat", **args)
super(**args.merge(animal: animal))
end
end
Pet.new(name: "Bob") # => #<struct Pet animal="Cat", name="Bob">
Solo Ruby 2.x (2.1 si desea los argumentos args obligatorios). Solo probado en MRI.
def Struct.new_with_kwargs(lamb)
members = lamb.parameters.map(&:last)
Struct.new(*members) do
define_method(:initialize) do |*args|
super(* lamb.(*args))
end
end
end
Foo = Struct.new_with_kwargs(
->(a, b=1, *splat, c:, d: 2, **kwargs) do
# must return an array with values in the same order as lambda args
[a, b, splat, c, d, kwargs]
end
)
Uso:
> Foo.new(-1, 3, 4, c: 5, other: ''foo'')
=> #<struct Foo a=-1, b=3, splat=[4], c=5, d=2, kwargs={:other=>"foo"}>
La desventaja menor es que debe asegurarse de que la lambda devuelve los valores en el orden correcto; la gran ventaja es que tienes todo el poder de las args de palabras clave de ruby 2.
Una solución que solo permite los argumentos de palabras clave de Ruby (Ruby> = 2.0).
class KeywordStruct < Struct
def initialize(**kwargs)
super(kwargs.keys)
kwargs.each { |k, v| self[k] = v }
end
end
Uso:
class Foo < KeywordStruct.new(:bar, :baz, :qux)
end
foo = Foo.new(bar: 123, baz: true)
foo.bar # --> 123
foo.baz # --> true
foo.qux # --> nil
foo.fake # --> NoMethodError
Este tipo de estructura puede ser realmente útil como un objeto de valor, especialmente si le gustan los accesadores de métodos más estrictos, que realmente generarán un error en lugar de devolver nil
(al estilo de OpenStruct).
esto no responde exactamente a la pregunta, pero encontré que funciona bien si tiene una cantidad de valores que desea estructurar. Tiene el beneficio de descargar la necesidad de recordar el orden de los atributos sin necesidad de subClass Struct.
MyStruct = Struct.new(:height, :width, :length)
hash = {height: 10, width: 111, length: 20}
MyStruct.new(*MyStruct.members.map {|key| hash[key] })