Search code examples
rubyhashenumerable

undefined method `assoc' for #<Hash:0x10f591518> (NoMethodError)


I'm trying to return a list of values based on user defined arguments, from hashes defined in the local environment.

def my_method *args
  #initialize accumulator
  accumulator = Hash.new(0)

  #define hashes in local environment
  foo=Hash["key1"=>["var1","var2"],"key2"=>["var3","var4","var5"]]
  bar=Hash["key3"=>["var6"],"key4"=>["var7","var8","var9"],"key5"=>["var10","var11","var12"]]
  baz=Hash["key6"=>["var13","var14","var15","var16"]]

  #iterate over args and build accumulator
  args.each do |x|
    if foo.has_key?(x)
        accumulator=foo.assoc(x)
    elsif bar.has_key?(x)
        accumulator=bar.assoc(x)
    elsif baz.has_key?(x)
        accumulator=baz.assoc(x)
    else
        puts "invalid input"
    end
end

  #convert accumulator to list, and return value
  return accumulator = accumulator.to_a {|k,v| [k].product(v).flatten}
end

The user is to call the method with arguments that are keywords, and the function to return a list of values associated with each keyword received.

For instance

> my_method(key5,key6,key1)
=> ["var10","var11","var12","var13","var14","var15","var16","var1","var2"]

The output can be in any order. I received the following error when I tried to run the code:

undefined method `assoc' for #<Hash:0x10f591518> (NoMethodError)

Please would you point me how to troubleshoot this? In Terminal assoc performs exactly how I expect it to:

> foo.assoc("key1")
=> ["var1","var2"]

Solution

  • I'm guessing you're coming to Ruby from some other language, as there is a lot of unnecessary cruft in this method. Furthermore, it won't return what you expect for a variety of reasons.

    `accumulator = Hash.new(0)`
    

    This is unnecessary, as (1), you're expecting an array to be returned, and (2), you don't need to pre-initialize variables in ruby.

    The Hash[...] syntax is unconventional in this context, and is typically used to convert some other enumerable (usually an array) into a hash, as in Hash[1,2,3,4] #=> { 1 => 2, 3 => 4}. When you're defining a hash, you can just use the curly brackets { ... }.

    For every iteration of args, you're assigning accumulator to the result of the hash lookup instead of accumulating values (which, based on your example output, is what you need to do). Instead, you should be looking at various array concatenation methods like push, +=, <<, etc.

    As it looks like you don't need the keys in the result, assoc is probably overkill. You would be better served with fetch or simple bracket lookup (hash[key]).

    Finally, while you can call any method in Ruby with a block, as you've done with to_a, unless the method specifically yields a value to the block, Ruby will ignore it, so [k].product(v).flatten isn't actually doing anything.

    I don't mean to be too critical - Ruby's syntax is extremely flexible but also relatively compact compared to other languages, which means it's easy to take it too far and end up with hard to understand and hard to maintain methods.

    There is another side effect of how your method is constructed wherein the accumulator will only collect the values from the first hash that has a particular key, even if more than one hash has that key. Since I don't know if that's intentional or not, I'll preserve this functionality.

    Here is a version of your method that returns what you expect:

    def my_method(*args)
      foo = { "key1"=>["var1","var2"],"key2"=>["var3","var4","var5"] }
      bar = { "key3"=>["var6"],"key4"=>["var7","var8","var9"],"key5"=>["var10","var11","var12"] }
      baz = { "key6"=>["var13","var14","var15","var16"] }
    
      merged = [foo, bar, baz].reverse.inject({}, :merge)
    
      args.inject([]) do |array, key|
        array += Array(merged[key])
      end
    end
    

    In general, I wouldn't define a method with built-in data, but I'm going to leave it in to be closer to your original method. Hash#merge combines two hashes and overwrites any duplicate keys in the original hash with those in the argument hash. The Array() call coerces an array even when the key is not present, so you don't need to explicitly handle that error.

    I would encourage you to look up the inject method - it's quite versatile and is useful in many situations. inject uses its own accumulator variable (optionally defined as an argument) which is yielded to the block as the first block parameter.