8

You can destructure an array by using the splat operator.

def foo(arg1, arg2, arg3)
  #...Do Stuff...
end
array = ['arg2', 'arg3']
foo('arg1', *array)

But is there a way to destruct a hash for option type goodness?

def foo(arg1, opts)
  #...Do Stuff with an opts hash...
end
opts = {hash2: 'bar', hash3: 'baz'}
foo('arg1', hash1: 'foo', *opts)

If not native ruby, has Rails added something like this?

Currently I'm doing roughly this with

foo('arg1', opts.merge(hash1: 'foo'))
Dylan Pierce
  • 4,313
  • 3
  • 35
  • 45
Drew
  • 15,158
  • 17
  • 68
  • 77

4 Answers4

6

It's 2018 and this deserves an update. Ruby 2.0 introduced keyword arguments and with that also the hash splat operator **. Now you can simply do the following:

def foo(arg1, opts)
  [arg1, opts]
end

opts = {hash2: 'bar', hash3: 'baz'}
foo('arg1', hash1: 'foo', **opts)
#=> ["arg1", {:hash1=>"foo", :hash2=>"bar", :hash3=>"baz"}]
3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • 1
    Keep in mind that this only works with symbol keys. `{**{a: 1}, **{b: 2}} #=> {:a=>1, :b=>2}` while `{**{'a' => 1}, **{'b' => 2}} #=> TypeError (wrong argument type String (expected Symbol))` – 3limin4t0r May 08 '19 at 22:18
  • Yep. I was looking to add some keys to a `model.as_json` call and used Rails' `symbolize_keys` method: `hash = {a: 1, b: 2, **model.as_json.symbolize_keys}` – Brian Jun 04 '20 at 19:47
5

Yes, there is a way to de-structure a hash:

def f *args; args; end
opts = {hash2: 'bar', hash3: 'baz'}
f *opts  #=> [[:hash2, "bar"], [:hash3, "baz"]]

The problem is that you what you want is actually not de-structuring at all. You’re trying to go from

'arg1', { hash2: 'bar', hash3: 'baz' }, { hash1: 'foo' }

(remember that 'arg1', foo: 'bar' is just shorthand for 'arg1', { foo: 'bar' }) to

'arg1', { hash1: 'foo', hash2: 'bar', hash3: 'baz' }

which is, by definition, merging (note how the surrounding structure—the hash—is still there). Whereas de-structuring goes from

'arg1', [1, 2, 3]

to

'arg1', 1, 2, 3
Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
3

There is no such thing (although it has been proposed). Since this will change the parsing rule, it cannot be implemented within Ruby. The best I can think of is to define * on hash like

class Hash; alias :* :merge end

and use it in one of the following ways:

foo('arg1', {hash1: 'foo'}*opts)
foo('arg1', {hash1: 'foo'} *opts)
foo('arg1', {hash1: 'foo'}. *opts)

the last of which I think is reasonably close to what you wanted.

sawa
  • 165,429
  • 45
  • 277
  • 381
  • Perhaps using `+` instead of `*` would make more sense? – Andrew Marshall Mar 03 '13 at 22:39
  • @AndrewMarshall I agree with that. I wanted to make it look close to the splat operator. – sawa Mar 03 '13 at 22:40
  • It's now possible in Ruby 3, though with a different/new syntax: `{a: 1, b: 2, c: 3, d: 4} => {a:, b:, **rest}` (see https://www.ruby3.dev/ruby-3-fundamentals/2021/01/06/everything-you-need-to-know-about-destructuring-in-ruby-3/) – Tyler Rick Feb 19 '21 at 19:20
1

If you're ok with using active_support:

require 'active_support/core_ext/hash/slice.rb'

def foo(*args)
  puts "ARGS: #{args}"
end

opts = {hash2: 'bar', hash3: 'baz'}
foo *opts.slice(:hash2, :hash3).values

...or you could monkey-patch your own solution:

class Hash
  def pluck(*keys)
    keys.map {|k| self[k] }
  end
end

def foo(*args)
  puts "ARGS: #{args}"
end

opts = {hash2: 'bar', hash3: 'baz'}
foo *opts.pluck(:hash2, :hash3)
soundly_typed
  • 39,257
  • 5
  • 28
  • 36