Home > other >  Ruby: Initializing a hash: Assign values, throw on undefined key access, and freeze, all at once?
Ruby: Initializing a hash: Assign values, throw on undefined key access, and freeze, all at once?

Time:03-22

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.

  •  Tags:  
  • ruby
  • Related