rubyruby-3

Default values when pattern matching a hash in Ruby 3


In Ruby 3,

hash => {a:}

works similarly to JS

{ a } = hash

except it throws an exception if :a isn't a key in hash instead of assigning a = nil. Ok, we can do

hash => {a:} rescue nil

except this gives undesired result if some keys are present and others missing:

{a: 1, b: 2} => {b:, c:} rescue nil
puts "b: #{b}, c: #{c}"

shows b and c are both nil instead of the desired b = 2, c = nil. Is there a simple way to get that result? Or even more generally, emulate JS hash destructuring with non-nil default values?


Solution

  • You can work around missing key error by making an object that responds to deconstruct_keys:

    hash = {a: 1}
    
    H(hash) => x:,y:,a:
    x  # => nil
    y  # => nil
    a  # => 1
    
    H(hash, "default") => x:,y:,a:
    x  # => "default"
    y  # => "default"
    a  # => 1
    
    H(hash, x: :z) => x:,y:,a:
    x  # => :z
    y  # => nil
    a  # => 1
    
    H(hash, "default", x: :z) => x:,y:,a:
    x  # => :z
    y  # => "default"
    a  # => 1
    

    deconstruct_keys is called when hash pattern is used => x:,y:,a:, these keys are passed as argument:

    # DelegateClass to make H be like a hash, but pattern match differently.
    # (it works just fine without DelegateClass)
    #
    #   H.new({a: 1}) # => {:a=>1}
    #
    class H < DelegateClass(Hash) 
      def initialize(hash, default = nil, **defaults)
        # since hash needs to be duped anyway
        @hash = defaults.merge hash               # defaults for specific key
        @default = default                        # default for any missing keys
        super(@hash)                              # get delegation going
      end
    
      def deconstruct_keys keys
        missing = keys - @hash.keys               # find what's missing
        missing.each { |k| @hash[k] = @default }  # add it to hash with default value
        @hash                                     # return for pattern matching
      end
    end
    
    # H() method, like a Hash() method.
    def H(...) 
      H.new(...)
    end
    

    https://docs.ruby-lang.org/en/3.2/syntax/pattern_matching_rdoc.html#label-Matching+non-primitive+objects-3A+deconstruct+and+deconstruct_keys