1

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?

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487

2 Answers2

1

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

Alex
  • 16,409
  • 6
  • 40
  • 56
0

While writing the question, I've thought of this workaround:

{a: some_default, **hash} => {a:}

or for the second case

{b: nil, c: nil, **hash} => {b:, c:}

I don't love it because it duplicates the keys and does some work to create the left-hand-side hash, and so hope someone has a better option.

Alexey Romanov
  • 167,066
  • 35
  • 309
  • 487