Named Parameters in Ruby Structs

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



Leave a reply



Submit