Reputation: 733
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
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