Ruby - Structs and named parameters inheritance
You could define new Structs, based in Person:
class Person < Struct.new(:name, :last_name)
end
class ReligiousPerson < Struct.new(*Person.members, :religion)
end
class PoliticalPerson < Struct.new(*Person.members, :political_affiliation)
end
### Main ###
person = Person.new('jackie', 'jack')
p pious_person = ReligiousPerson.new('billy', 'bill', 'Zoroastrianism')
p political_person = PoliticalPerson.new('frankie', 'frank', 'Connecticut for Lieberman')
Result:
#<struct ReligiousPerson name="billy", last_name="bill", religion="Zoroastrianism">
#<struct PoliticalPerson name="frankie", last_name="frank", political_affiliation="Connecticut for Lieberman">
Immediate after posting my answer I had an idea:
class Person < Struct.new(:name, :last_name)
def self.derived_struct( *args )
Struct.new(*self.members, *args)
end
end
class ReligiousPerson < Person.derived_struct(:religion)
end
class PoliticalPerson < Person.derived_struct(:political_affiliation)
end
### Main ###
person = Person.new('jackie', 'jack')
p pious_person = ReligiousPerson.new('billy', 'bill', 'Zoroastrianism')
p political_person = PoliticalPerson.new('frankie', 'frank', 'Connecticut for Lieberman')
Works fine!
You may also add #derived_struct to Struct:
class Struct
def self.derived_struct( *args )
Struct.new(*self.members, *args)
end
end
How create new instance of a Ruby Struct using named arguments (instead of assuming correct order of arguments)
Named parameters are not (yet) possible in Ruby's Struct
class. You can create a subclass of your own in line with this Gist: https://gist.github.com/mjohnsullivan/951668
As you know, full-fledged Classes
can have named parameters. I'd love to learn why they aren't possible with Structs
... I surmise that someone on the Core team has thought of this and rejected it.
How to define a Ruby Struct which accepts its initialization arguments as a hash?
Cant you just do:
def initialize(hash)
hash.each do |key, value|
send("#{key}=", value)
end
end
UPDATE:
To specify default values you can do:
def initialize(hash)
default_values = {
first_name: ''
}
default_values.merge(hash).each do |key, value|
send("#{key}=", value)
end
end
If you want to specify that given attribute is required, but has no default value you can do:
def initialize(hash)
requried_keys = [:id, :username]
default_values = {
first_name: ''
}
raise 'Required param missing' unless (required_keys - hash.keys).empty?
default_values.merge(hash).each do |key, value|
send("#{key}=", value)
end
end
Directly pass struct to method with matching keyword arguments
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.
Passing `nil` to method using default named parameters
If you want to follow a Object-Oriented approach, you could isolate your defaults in a separate method and then use Hash#merge
:
class Taco
def initialize (args)
args = defaults.merge(args)
@meat = args[:meat]
@cheese = args[:cheese]
@salsa = args[:salsa]
end
def assemble
"taco with: #{@meat} + #{@cheese} + #{@salsa}"
end
def defaults
{meat: 'steak', cheese: true, salsa: 'spicy'}
end
end
Then following the suggestion by @sawa (thanks), use Rails' Hash#compact
for your input hashes that have explicitly defined nil
values and you will have the following output:
taco with: chicken + false + mild
taco with: steak + true + spicy
taco with: pork + true + spicy
EDIT:
If you do not want to use Rails' wonderful Hash#compact
method, you can use Ruby's Array#compact
method. Replacing the first line within the initialize
method to:
args = defaults.merge(args.map{|k, v| [k,v] if v != nil }.compact.to_h)
Ruby Struct and OStruct: How can I require parameters
Sure, just handle it like a class
MyStruct = Struct.new(:param1, :param2, :param3) do |params|
def initialize *args
raise "params are required #{[:param1, :param2, :param3]} - #{args}}" unless (args.length == 3)
end
end
str = MyStruct.new(1,2)
p str
# in `initialize': params are required [:param1, :param2, :param3] - [1, 2]} (RuntimeError)
Related Topics
Resetting a Singleton Instance in Ruby
How to Get Ruby to Parse Time as If It Were in a Different Time Zone
Certificate Verify Failed in "Gem Install Foundation"
Firebase Token Error, "The Custom Token Corresponds to a Different Audience."
In Ruby, How to I Control the Order in Which Test::Unit Tests Are Run
Other Ruby Map Shorthand Notation
How to Get a List of All Available Rake Tasks in a Namespace
Sinatra with a Persistent Variable
How to Create a List of Months Between Two Dates in Rails
In Ruby, How to Read Data Column Wise from a CSV File
How to Use Rspec Expectations in Irb
Constants or Class Variables in Ruby
Deleting While Iterating in Ruby