In Ruby, I want to initialize a new hash, such that:
- The hash is assigned a specific set of initial key-value pairs;
- The hash is configured to raise an error if an attempt is made to retrieve the value for an undefined key;
- The hash is frozen (can't be further modified).
Is there an elegant Ruby-ish way to do this setup all at once?
I'm aware that this can be done in three separate lines, e.g.:
COIN_SIDES = { heads: 'heads', tails: 'tails' }
COIN_SIDES.default_proc = -> (h, k) { raise KeyError, "Key '#{k}' not found" }
COIN_SIDES.freeze
CodePudding user response:
You can do this by initializing hash with default_proc and then adding components with merge!:
h = Hash.new{|hash, key| raise KeyError, "Key '#{key}' not found"}.merge!({ heads: 'heads', tails: 'tails' }).freeze
CodePudding user response:
I'm not sure that this is terribly elegant, but one way to achieve this in one (long) line is by using .tap
:
COIN_SIDES = { heads: 'heads', tails: 'tails' }.tap { |cs| cs.default_proc = -> (h, k) { raise KeyError, "Key '#{k}' not found" } }.tap(&:freeze)
This approach does at least avoid the RuboCop: Freeze mutable objects assigned to constants [Style/MutableConstant]
warning generated when running the RuboCop linter on the 3-line version of the code from the original question, above.
CodePudding user response:
You can accomplish most of this functionality by making a custom class, the only downside being it's not really a hash, so you'd need to explicitly add on extra functionality like .keys
, each
, etc if needed:
class HashLike
def initialize(hsh)
singleton_class.attr_reader *hsh.keys
hsh.each { |k,v| instance_variable_set "@#{k}", v }
end
end
hashlike = HashLike.new(some_value: 1)
hashlike.some_value # 1
hashlike.missing_value # NoMethodError
hashlike.some_value = 2 # NoMethodError
Another similar way:
class HashLike2
def initialize(hsh)
@hsh = hsh
end
def [](key)
@hsh.fetch(key)
end
end
hashlike2 = HashLike2.new(some_value: 1)
hashlike2[:some_value] # 1
hashlike2[:missing_value] # KeyError
hashlike2[:some_value] = 2 # NoMethodError
But in my opinion, there's not much a reason to do this. You can easily move your original 3 lines into a method somewhere out of the way, and then it doesn't matter if it's 3 lines or 1 anymore.