-1

I have a hash, whose values are an array of size 1:

hash = {:start => [1]}

I want to unpack the arrays as in:

hash.each_pair{ |key, value| hash[key] = value[0] } # => {:start=>1}

and I thought the *-operator as in the following would work, but it does not give the expected result:

hash.each_pair{ |key, value| hash[key] = *value } # => {:start=>[1]}

Why does *value return [1] and not 1?

miken32
  • 42,008
  • 16
  • 111
  • 154
SimonH
  • 1,385
  • 15
  • 35

3 Answers3

4

Because the []= method applied to hash takes only one argument in addition to the key (which is put inside the [] part), and a splatted/expanded array, which is in general a sequence of values (which coincidentally happens to be a single element in this particular case) cannot be directly accepted as the argument as is splatted. So it is accepted by the argument of []= as an array after all.

In other words, an argument (of the []= method) must be an object, but splatted elements (such as :foo, :bar, :baz) are not an object. The only way to interpret them as an object is to put them back into an array (such as [:foo, :bar, :baz]).

Using the splat operator, you can do it like this:

hash.each_pair{|key, value| hash.[]= key, *value}
sawa
  • 165,429
  • 45
  • 277
  • 381
  • 1
    It looks less weird if you use `store`, i.e. `hash.each_pair{|key, values| hash.store(key, *values)}`. However, both cases will raise an argument error if the array contains no elements or more than one element. – Stefan Sep 15 '15 at 14:35
4

sawa and Ninigi already pointed out why the assignment doesn't work as expected. Here's my attempt.

Ruby's assignment features work regardless of whether you're assigning to a variable, a constant or by implicitly invoking an assignment method like Hash#[]= with the assignment operator. For the sake of simplicity, I'm using a variable in the following examples.

Using the splat operator in an assignment does unpack the array, i.e.

a = *[1, 2, 3]

is evaluated as:

a = 1, 2, 3

But Ruby also allows you to implicitly create arrays during assignment by listing multiple values. Therefore, the above is in turn equivalent to:

a = [1, 2, 3]

That's why *[1] results in [1] - it's unpacked, just to be converted back to an array.

Elements can be assigned separately using multiple assignment:

a, b = [1, 2, 3]
a #=> 1
b #=> 2

or just:

a, = [1, 2, 3]
a #=> 1

You could use this in your code (note the comma after hash[key]):

hash = {:start => [1]}
hash.each_pair { |key, values| hash[key], = values }
#=> {:start=>1}

But there's another and more elegant way: you can unpack the array by putting parentheses around the array argument:

hash = {:start => [1]}
hash.each_pair { |key, (value)| hash[key] = value }
#=> {:start=>1}

The parentheses will decompose the array, assigning the first array element to value.

Stefan
  • 109,145
  • 14
  • 143
  • 218
  • Whenever I see that you posted an answer, I know before reading it that it is going to be the best one. – sawa Sep 15 '15 at 15:22
3

Because Ruby is acting unexpectedly smart here.

True, the splash operator will "fold" and "unfold" an array, but the catch in your code is what you do with that fanned value.

Take this code into account:

array = ['a', 'b']
some_var = *array
array # => ['a', 'b']

As you can see the splat operator seemingly does nothing to your array, while this:

some_var, some_other_var = *array
some_var # => "a"
somet_other_var # => "b"

Will do what you'd expect it does.

It seems ruby just "figures" if you splat an array into a single variable, that you want the array, not the values.

EDIT: As sawa pointed out in the comments, hash[key] = is not identical to variable =. []= is an instance Method of Hash, with it's own C-Code under the hood, which COULD (in theory) lead to different behaviour in some instances. I don't know of any example, but that does not mean there is none. But for the sake of simplicity, we can asume that the regular variable assignment behaves exactly identical to hash[key] =.

Ninigi
  • 1,311
  • 12
  • 19
  • 1
    Note that `Hash#[]=` is not the same as variable assignment `=`, although they are similar in some respects. – sawa Sep 15 '15 at 11:21
  • @sawa how do variable assignment and assignment methods differ? – Stefan Sep 15 '15 at 12:09
  • 1
    @Stefan I just wanted to mention that they are different things. One is built-in, and one is a hash method. I am not denying that their syntax is similar. Unless this answer makes it clear that what applies to `=` carries over to `Hash#[]=`, it is not a complete answer. – sawa Sep 15 '15 at 12:20
  • 1
    @Stefan As @sawa said, it's just not the same, period. The C-Code for the `Hash#[]=` method is more "complex" than a regular assignment, which COULD lead to different behaviour in some instances. I can't think of any example, but that doesn't mean there is none :) – Ninigi Sep 15 '15 at 12:26
  • Of course they are different - a variable is not a method. But isn't Ruby evaluating the right-hand side of an assignment (including the splat operator) before performing the assignment / method invocation? I would expect Ruby to behave consistently in this respect, regardless of whether the left-hand side is a variable or a method, let alone the method's implementation. – Stefan Sep 15 '15 at 12:49
  • @Stefan the important thing is, []= is not a regular assignment, it is a method defined in the Hash class. Say you define a method `def variable=(thing); puts thing; end` it would be used like `variable = "some string` but would not assign variable to "somet string"... That's pretty much what happens in the Hash class only in C-Code, not Ruby :) – Ninigi Sep 15 '15 at 13:21
  • @Ninigi I know, but `Hash#[]=` (or `#variable=`) is not responsible for handling the splat operator, it is not even aware of it. Turning `*value` into an argument list and wrapping it in an array (as needed) is handled by Ruby prior to the method invocation (or variable assignment). Therefore, I don't think the kind of assignment is relevant at all. – Stefan Sep 15 '15 at 13:37
  • 1
    @Stefan that doesn't change the fact that those two methods handle their arguments differently, which COULD lead to different behaviour for a list of arguments compared to an array... The splat operator isn't what changes its behaviour, it's the method which receives the evaluated result. – Ninigi Sep 15 '15 at 13:42
  • Let me put it another way: assignments behave identical in respect to the splat operator. – Stefan Sep 15 '15 at 14:20
  • @Stefan yes, that's pretty much the gist of it. – Ninigi Sep 15 '15 at 14:36
  • @Stefan Making `[]=` method look similar to (or even assimilating it with) assignment (with respect to reaction to splat) is indeed done by Ruby prior to and independent of method invocation, as you pointed out, but that does not change the fact that it needs mentioning. This is all due to how Ruby's syntax sugar works, and it could have been in another way. Ninigi edited the answer correctly. Now the answer is fine. – sawa Sep 15 '15 at 15:19