Mixed Argument Struct
Ruby is all about giving you choices. What if you could give users of your code the choice between whether to initialize objects with positional or keyword arguments, or some mix of the two? In today’s episode we’ll see how one short line of Ruby code can provide this ability to a Struct-based class.
Video transcript & code
Mixed Argument Struct
Once upon a time, Ruby Structs were initialized with positional arguments only
Room = Struct.new(:name, :north, :south, :east, :west) do
def inspect
"Room<#{name}>"
end
end
pantry = Room.new("pantry")
dining = Room.new("dining room")
kitchen = Room.new("kitchen", nil, pantry, nil, dining)
Since Ruby 2.5, Structs can be initialized with keyword args
Room = Struct.new(:name, :north, :south, :east, :west, keyword_init: true) do
def inspect
"Room<#{name}>"
end
end
pantry = Room.new(name: "pantry")
dining = Room.new(name: "dining room")
kitchen = Room.new(name: "kitchen", south: pantry, west: dining)
kitchen # => Room
kitchen.south # => Room
kitchen.west # => Room
But enabling keywords prevents the use of positional args.
What if we want a shorthand version?
pantry = Room.new("pantry"] # ~> ArgumentError: wrong number of arguments (given 1, expected 0)
dining = Room.new("dining room")
kitchen = Room.new("kitchen", south: pantry, west: dining)
kitchen # =>
kitchen.south # =>
kitchen.west # =>
# ~> ArgumentError
# ~> wrong number of arguments (given 1, expected 0)
How could we get the best of both worlds?
pantry = Room.new("pantry")
dining = Room.new("dining room")
kitchen = Room.new("kitchen", south: pantry, west: dining)
We start with a custom constructor
Which just passes kwargs through
def initialize(**kwargs)
kwargs
# => {:name=>"pantry"}
# ,{:name=>"dining room"}
# ,{:name=>"kitchen",
# :south=>
# #,
# :west=>
# #}
super(**kwargs)
end
Then add a collector parameter for keyword arguments
def initialize(*args, **kwargs)
args # => [], [], []
kwargs
# => {:name=>"pantry"}
# ,{:name=>"dining room"}
# ,{:name=>"kitchen",
# :south=>
# #,
# :west=>
# #}
super(**kwargs)
end
The members
method gives us a list of attribute names
def initialize(*args, **kwargs)
args
members
# => [:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
super(**kwargs)
end
It can be zipped together with positional arguments
def initialize(*args, **kwargs)
args # => [], [], []
members
# => [:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
members.zip(args)
# => [[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
# ,[[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
# ,[[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
super(**kwargs)
end
And converted into a hash
def initialize(*args, **kwargs)
args # => [], [], []
members
# => [:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
members.zip(args)
# => [[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
# ,[[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
# ,[[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
members.zip(args).to_h
# => {:name=>nil, :north=>nil, :south=>nil, :east=>nil, :west=>nil}
# ,{:name=>nil, :north=>nil, :south=>nil, :east=>nil, :west=>nil}
# ,{:name=>nil, :north=>nil, :south=>nil, :east=>nil, :west=>nil}
super(**kwargs)
end
And the keyword arguments can then be merged into it
This gives us a complete set of keyword arguments
def initialize(*args, **kwargs)
args # => [], [], []
kwargs
# => {:name=>"pantry"}
# ,{:name=>"dining room"}
# ,{:name=>"kitchen",
# :south=>
# #,
# :west=>
# #}
members
# => [:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
# ,[:name, :north, :south, :east, :west]
members.zip(args)
# => [[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
# ,[[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
# ,[[:name, nil], [:north, nil], [:south, nil], [:east, nil], [:west, nil]]
members.zip(args).to_h
# => {:name=>nil, :north=>nil, :south=>nil, :east=>nil, :west=>nil}
# ,{:name=>nil, :north=>nil, :south=>nil, :east=>nil, :west=>nil}
# ,{:name=>nil, :north=>nil, :south=>nil, :east=>nil, :west=>nil}
members.zip(args).to_h.merge(kwargs)
# => {:name=>"pantry", :north=>nil, :south=>nil, :east=>nil, :west=>nil}
# ,{:name=>"dining room", :north=>nil, :south=>nil, :east=>nil, :west=>nil}
# ,{:name=>"kitchen",
# :north=>nil,
# :south=>
# #,
# :east=>nil,
# :west=>
# #}
super(**kwargs)
end
Let's use this as the input to super
def initialize(*args, **kwargs)
super(**members.zip(args).to_h.merge(kwargs))
end
And then switch the room names to positional
Room = Struct.new(:name, :north, :south, :east, :west, keyword_init: true) do
def inspect
"Room<#{name}>"
end
def initialize(*args, **kwargs)
super(**members.zip(args).to_h.merge(kwargs))
end
end
pantry = Room.new("pantry")
dining = Room.new("dining room")
kitchen = Room.new("kitchen", south: pantry, west: dining)
kitchen # => Room
kitchen.south # => Room
kitchen.west # => Room
We have successfully mixed positional and keyword arguments!
Responses