Named Parameters in Ruby Structs
Solution 1
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">
Solution 2
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">
Solution 3
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">
Solution 4
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).
Solution 5
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
Matt S.
Updated on June 07, 2022Comments
-
Matt S. about 2 years
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.