dillon
dillon

Reputation: 733

Directly pass struct to method with matching keyword arguments

As the title suggests, I'm attempting to pass a struct to a method with keyword arguments. As ruby is generally pretty terse, I was hoping there is a less verbose method of implementing what I have thus far.

Here is the class that I'm working with

class WheelStore
  attr_reader :wheels

  def initialize(data: [])
    @wheels = wheelify data
  end

  def diameters
    wheels.map { |wheel| diameter rim: wheel.rim, tire: wheel.tire }
  end

  private

  def diameter(rim:, tire:)
    rim + (tire * 2)
  end

  Wheel = Struct.new :rim, :tire, keyword_init: true
  def wheelify(data)
    data.map { |item| Wheel.new rim: item[0], tire: item[1] }
  end
end

puts WheelStore.new(data: [[10, 2], [3, 2]]).diameters

The question is with regards to the diameter method which expects a rim and a tire. I'd like for the caller to be able to just pass the wheel, which is a Wheel struct that implements rim & tire, to the diameter method.

# verbose
def diameters
  wheels.map { |wheel| diameter rim: wheel.rim, tire: wheel.tire }
end
#better
def diameters
  wheels.map { |wheel| diameter wheel }
end
#best
def diameters
  wheels.map { diameter }
end

I'm curious, is it just a language construct for why we can't implement the last 2 versions of this diameters method?

For reference, I do a lot of Javascript development and what I'm attempting here is very similar to Javascript's Destructuring assignment

Upvotes: 1

Views: 238

Answers (1)

Schwern
Schwern

Reputation: 164689

The problem is WheelStore is doing procedural work for Wheel. You need a proper Wheel class.

class Wheel
  attr_reader :rim, :tire

  def initialize(rim:, tire:)
    @rim = rim
    @tire = tire
  end

  def diameter
    rim + (tire * 2)
  end
end

Then wheelify goes away. Instead, use Wheel.new directly. Only initialize with proper Wheel objects. This is more flexible and efficient. Instead of first transforming the data from the original format into your Array of Arrays format and then again into key/value pairs, it only has to be transformed once into key/value pairs.

class WheelStore
  attr_reader :wheels

  def initialize(wheels: [])
    @wheels = wheels
  end

  def diameters
    wheels.map { |wheel| wheel.diameter }
  end
end

datum = [["rim1", "tire1"], ["rim2", "tire2"]]
wheel_store1 = WheelStore.new(
  wheels: datum.map { |data| Wheel.new(rim: data[0], tire: data[1]) }
)

datum = [
  { rim: "rim1", tire: "tire1" },
  { rim: "rim2", tire: "tire2" }
]
wheel_store2 = WheelStore.new(
  wheels: datum.map { |data| Wheel.new(**data) }
)

If you have specific data formats which you commonly turn into Wheels, then you would make a class method of Wheel to deal with them.

class Wheel
  class << self
    def new_from_array(wheel_data)
      new(rim: wheel_data[0], tire: wheel_data[1])
    end
  end
end

datum = [["rim1", "tire1"], ["rim2", "tire2"]]
ws = WheelStore.new(
  wheels: datum.map { |data| Wheel.new_from_array(data) }
)

If importing Wheels gets sufficiently complex, you might write a WheelImporter.


You might be assuming that a Struct will be more efficient than a named class, but in Ruby it isn't. It's pretty much exactly the same in terms of performance.

2.6.5 :001 > Wheel = Struct.new :rim, :tire, keyword_init: true
 => Wheel(keyword_init: true) 
2.6.5 :002 > Wheel.class
 => Class 

#best
def diameters
  wheels.map { diameter }
end

2.7 added Numbered Parameters.

wheels.map { _1.diameter }

Prior to 2.7 you must declare the arguments.

You can hack around this with instance_eval and create your own implicit versions of Enumerable methods. This turns each element into the subject of the block, self.

module Enumerable
  def implied_map(&block)
    map { |a| a.instance_eval(&block) }
  end
end

[1,2,3].implied_map { to_s }       # ["1", "2", "3"]
[1,2,3].implied_map { self.to_s }  # same

...but don't do that. instance_eval is useful for writing DSLs. But making your own versions of standard methods means you're creating your own little off-shoot of Ruby that other people will need to learn.

Upvotes: 3

Related Questions