Named Parameters in Ruby Structs

☆樱花仙子☆ 提交于 2019-11-30 05:44:52

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

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.

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