TL;DR
I solved the problem through trial and error, but there's clearly a gap in my understanding of how the splat operator and pp
method are consistently giving me a different object than the one I think I have. I'd like to understand that gap, and identify a better way to merge an array of hashes. I'd also like to be able to debug this sort of thing more effectively in the future.
Code samples and debugging steps are first. My semi-satisfactory solution and more-detailed question are at the bottom.
The Code
I'm using MRI Ruby 2.6.2. Given class Foo, I expect Foo#windows to return a merged hash. Here is a minimal example of the class:
class Foo
attr_reader :windows
def initialize
@windows = {}
end
def pry
{ pry: "stuff pry\r" }
end
def irb
{ irb: "stuff irb\r" }
end
def windows= *hashes
@windows.merge! *hashes
end
end
foo = Foo.new
foo.windows = foo.pry, foo.irb
The Problem (with Debugging)
However, trying to assign to foo.windows (or even trying to be less ambiguous with to help out the parser with foo.windows= foo.pry, foo.irb
) I get an exception from the REPL:
TypeError: no implicit conversion of Array into Hash
However, if I modify the instance with a singleton method to capture the value of the *hashes
argument, I see an array of hashes that I can merge just fine. Consider the following:
def foo.windows= *hashes
pp *hashes
end
foo.windows = foo.pry, foo.irb
#=> [{:pry=>"stuff pry\r"}, {:irb=>"stuff irb\r"}]
{}.merge *[{:pry=>"stuff pry\r"}, {:irb=>"stuff irb\r"}]
#=> {:pry=>"stuff pry\r", :irb=>"stuff irb\r"}
Grabbing the output from #pp gives me something that works as expected. And yet, when I dig a little deeper, it turns out that something is layering on an extra nesting of the Hash:
def foo.windows= *hashes
pp *hashes.inspect
end
foo.windows = foo.pry, foo.irb
"[[{:pry=>\"stuff pry\\r\"}, {:irb=>\"stuff irb\\r\"}]]"
Even though the return value doesn't show it, there's an extra set of square brackets causing the array to be nested. I don't really understand where they're coming from.
What Works
So, for whatever reason, I have to splat the array, flatten it, and then I'm able to merge:
def foo.windows= *hashes
@windows.merge! *hashes.flatten
end
# The method still returns unexpected results here, but...
foo.windows = foo.pry, foo.irb
#=> [{:pry=>"stuff pry\r"}, {:irb=>"stuff irb\r"}]
# the value stored in the instance variable is finally correct!
foo.windows
#=> {:pry=>"stuff pry\r", :irb=>"stuff irb\r"}
But Why?
So yes, I've managed to solve the problem. However, my question is really about why merging the hashes doesn't work as expected, and where the extra layer of nesting is coming from. I'm not expecting an Array of Array of Hashes, but rather an Array of Hashes. Is there a gap in my understanding, or is this a weird edge case of some sort?
More importantly, why is this so difficult to debug? I'd expect #pp or #inspect to show me the object I really have ahold of, rather than show me an Array of Hashes as the return value when I clearly have an Array of Arrays containing the Hash.