I'm pretty new to Ruby so apologies if this is an obvious question.
I'd like to use named parameters when instantiating a Struct, i.e. be able to specify which items in the Struct get what values, and default the rest to nil.
For example I want to do:
Movie = Struct.new :title, :length, :rating
m = Movie.new :title => 'Some Movie', :rating => 'R'
This doesn't work.
So I came up with the following:
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'
This seems to work just fine, but I'm not sure if there's a better way of doing this, or if I'm doing something pretty insane. If anyone can validate/rip apart this approach, I'd be most grateful.
UPDATE
I ran this initially in 1.9.2 and it works fine; however having tried it in other versions of Ruby (thank you rvm), it works/doesn't work as follows:
- 1.8.7: Not working
- 1.9.1: Working
- 1.9.2: Working
- JRuby (set to run as 1.9.2): not working
JRuby is a problem for me, as I'd like to keep it compatible with that for deployment purposes.
YET ANOTHER UPDATE
In this ever-increasing rambling question, I experimented with the various versions of Ruby and discovered that Structs in 1.9.x store their members as symbols, but in 1.8.7 and JRuby, they are stored as strings, so I updated the code to be the following (taking in the suggestions already kindly given):
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'
This now appears to work for all the flavours of Ruby that I've tried.
Synthesizing the existing answers reveals a much simpler option for Ruby 2.0+:
class KeywordStruct < Struct
def initialize(**kwargs)
super(*members.map{|k| kwargs[k] })
end
end
Usage is identical to the existing Struct
, where any argument not given will default to 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">
If you want to require the arguments like Ruby 2.1+'s required kwargs, it's a very small change:
class RequiredKeywordStruct < Struct
def initialize(**kwargs)
super(*members.map{|k| kwargs.fetch(k) })
end
end
At that point, overriding initialize
to give certain kwargs default values is also doable:
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">
The less you know, the better. No need to know whether the underlying data structure uses symbols or string, or even whether it can be addressed as a Hash
. Just use the attribute setters:
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
It takes both positional and keyword arguments:
> KwStruct.new "q", :zxcv => "z"
=> #<struct KwStruct qwer="q", asdf=nil, zxcv="z">
A solution that only allows Ruby keyword arguments (Ruby >=2.0).
class KeywordStruct < Struct
def initialize(**kwargs)
super(kwargs.keys)
kwargs.each { |k, v| self[k] = v }
end
end
Usage:
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
This kind of structure can be really useful as a value object especially if you like more strict method accessors which will actually error instead of returning nil
(a la OpenStruct).
Have you considered 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
You could rearrange the 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
Based on @Andrew Grimm's answer, but using Ruby 2.0's keyword arguments:
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
Note that this does not allow mixing of regular and keyword arguments-- you can only use one or the other.
If your hash keys are in order you can call the splat operator to the rescue:
NavLink = Struct.new(:name, :url, :title)
link = {
name: 'Stack Overflow',
url: 'https://stackoverflow.com',
title: 'Sure whatever'
}
actual_link = NavLink.new(*link.values)
#<struct NavLink name="Stack Overflow", url="https://stackoverflow.com", title="Sure whatever">
If you do need to mix regular and keyword arguments, you can always construct the initializer by hand...
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">
This has the title as a mandatory first argument just for illustration. It also has the advantage that you can set defaults for each keyword argument (though that's unlikely to be helpful if dealing with Movies!).
For a 1-to-1 equivalent with the Struct behavior (raise when the required argument is not given) I use this sometimes (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
and from there on
class SimpleExecutor < Struct.keyed :foo, :bar
...
end
This will raise a KeyError
if you missed an argument, so real nice for stricter constructors and constructors with lots of arguments, data transfer objects and the like.
With newer versions of Ruby you can use keyword_init: true
:
Movie = Struct.new(:title, :length, :rating, keyword_init: true)
Movie.new(title: 'Title', length: '120m', rating: 'R')
# => #<struct Movie title="Title", length="120m", rating="R">
this doesn't exactly answer the question but I found it to work well if you have say a hash of values you wish to structify. It has the benefit of offloading the need to remember the order of attributes while also not needing to subClass Struct.
MyStruct = Struct.new(:height, :width, :length)
hash = {height: 10, width: 111, length: 20}
MyStruct.new(*MyStruct.members.map {|key| hash[key] })
Ruby 2.x only (2.1 if you want required keyword args). Only tested in 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
)
Usage:
> Foo.new(-1, 3, 4, c: 5, other: 'foo')
=> #<struct Foo a=-1, b=3, splat=[4], c=5, d=2, kwargs={:other=>"foo"}>
The minor downside is that you have to ensure the lambda returns the values in the correct order; the big upside is that you have the full power of ruby 2's keyword args.
来源:https://stackoverflow.com/questions/5407940/named-parameters-in-ruby-structs