Named Parameters in Ruby Structs

2019-02-05 14:09发布

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.

11条回答
来,给爷笑一个
2楼-- · 2019-02-05 14:26

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.

查看更多
Explosion°爆炸
3楼-- · 2019-02-05 14:27

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
查看更多
虎瘦雄心在
4楼-- · 2019-02-05 14:28

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

查看更多
【Aperson】
5楼-- · 2019-02-05 14:33

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.

查看更多
ら.Afraid
6楼-- · 2019-02-05 14:35

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
查看更多
登录 后发表回答