Named Parameters in Ruby Structs

南笙酒味 提交于 2019-12-30 01:37:07

问题


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.


回答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">



回答2:


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">



回答3:


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).




回答4:


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



回答5:


You could rearrange the ifs.

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



回答6:


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.




回答7:


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"> 



回答8:


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!).




回答9:


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.




回答10:


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">



回答11:


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] })




回答12:


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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!