In Progress
Unit 1, Lesson 1
In Progress

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