Reputation: 311526
Struct
lets me create a new class that takes arguments and has some nice semantics. However, the arguments aren't required, and their order requires consulting the definition:
Point = Struct.new(:x, :y)
Point.new(111, 222)
#=> <point instance with x = 111, y = 222>
Point.new(111)
#=> <point instance with x = 111, y = nil>
I'd like something similar to a Struct, but which uses keyword arguments instead:
Point = StricterStruct.new(:x, :y)
Point.new(x: 111, y: 222)
#=> <point instance with x = 111, y = 222>
Point.new(x: 111)
#=> ArgumentError
That might look something like this:
module StricterStruct
def self.new(*attributes)
klass = Class.new
klass.instance_eval { ... }
klass
end
end
But what should go in the braces to define an initialize
method on klass
such that:
attributes
; andinitialize
method assigns them to instance variables of the same nameUpvotes: 8
Views: 930
Reputation: 667
I was also hunting around for this, and eventually stumbled across this gem that does exactly this:
https://github.com/etiennebarrie/kwattr
class FooBar
kwattr :foo, bar: 21
end
foobar = FooBar.new(foo: 42) # => #<FooBar @foo=42, @bar=21>
foobar.foo # => 42
foobar.bar # => 21
instead of
class FooBar
attr_reader :foo, :bar
def initialize(foo:, bar: 21)
@foo = foo
@bar = bar
end
end
Upvotes: 0
Reputation: 311526
I wound up using a (surprisingly Pythonic) **kwargs
strategy, thanks to the new features in Ruby 2.0+:
module StricterStruct
def self.new(*attribute_names_as_symbols)
c = Class.new
l = attribute_names_as_symbols
c.instance_eval {
define_method(:initialize) do |**kwargs|
unless kwargs.keys.sort == l.sort
extra = kwargs.keys - l
missing = l - kwargs.keys
raise ArgumentError.new <<-MESSAGE
keys do not match expected list:
-- missing keys: #{missing}
-- extra keys: #{extra}
MESSAGE
end
kwargs.map do |k, v|
instance_variable_set "@#{k}", v
end
end
l.each do |sym|
attr_reader sym
end
}
c
end
end
Upvotes: 6
Reputation: 29328
I might be misunderstanding the question but are you looking for something like this?
module StricterStruct
def self.new(*attributes)
klass = Class.new
klass.class_eval do
attributes.map!{|n| n.to_s.downcase.gsub(/[^\s\w\d]/,'').split.join("_")}
define_method("initialize") do |args|
raise ArgumentError unless args.keys.map(&:to_s).sort == attributes.sort
args.each { |var,val| instance_variable_set("@#{var}",val) }
end
attr_accessor *attributes
end
klass
end
end
Then
Point = StricterStruct.new(:x,:y)
#=> Point
p = Point.new(x: 12, y: 77)
#=> #<Point:0x2a89400 @x=12, @y=77>
p2 = Point.new(x: 17)
#=> ArgumentError
p2 = Point.new(y: 12)
#=> ArgumentError
p2 = Point.new(y:17, x: 22)
#=> #<Point:0x28cf308 @y=17, @x=22>
If you want something more please explain as I think this meets your criteria at least my understanding of it. As it defines the methods and can take a "keyword"(Hash
) argument and assign the proper instance variables.
If you want the arguments to be specified in the same order as they were defined just remove the sorts.
Also there might be cleaner implementations.
Upvotes: 1
Reputation: 160551
It sounds like you're looking for Ruby's built-in OpenStruct:
require 'ostruct'
foo = OpenStruct.new(bar: 1, 'baz' => 2)
foo # => #<OpenStruct bar=1, baz=2>
foo.bar # => 1
foo[:bar] # => 1
foo.baz # => 2
foo.baz = 3
foo # => #<OpenStruct bar=1, baz=3>
I think of OpenStruct as candy-coating on a Hash, where we can access and assign to the instance without any real constraints, unlike creating a real class with the normal accessors. We can pretend it's a hash, or a class with methods. It's a dessert topping, no it's a floor-wax, no, it's two things in one!
Upvotes: 0