Ruby - Keyword Arguments - Can you treat all of the keyword arguments as a hash? How?

Jesse Farmer picture Jesse Farmer · Feb 25, 2014 · Viewed 8k times · Source

I have a method that looks like this:

def method(:name => nil, :color => nil, shoe_size => nil) 
  SomeOtherObject.some_other_method(THE HASH THAT THOSE KEYWORD ARGUMENTS WOULD MAKE)
end

For any given call, I can accept any combination of optional values. I like the named arguments, because I can just look at the method's signature to see what options are available.

What I don't know is if there is a shortcut for what I have described in capital letters in the code sample above.

Back in the olden days, it used to be:

def method(opts)
  SomeOtherObject.some_other_method(opts)
end

Elegant, simple, almost cheating.

Is there a shortcut for those Keyword Arguments or do I have to reconstitute my options hash in the method call?

Answer

Dennis picture Dennis · Oct 24, 2014

Yes, this is possible, but it's not very elegant.

You'll have to use the parameters method, which returns an array of the method's parameters and their types (in this case we only have keyword arguments).

def foo(one: 1, two: 2, three: 3)
  method(__method__).parameters
end  
#=> [[:key, :one], [:key, :two], [:key, :three]]

Knowing that, there's various ways how to use that array to get a hash of all the parameters and their provided values.

def foo(one: 1, two: 2, three: 3)
  params = method(__method__).parameters.map(&:last)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}

So your example would look like

def method(name: nil, color: nil, shoe_size: nil)
  opts = method(__method__).parameters.map(&:last).map { |p| [p, eval(p.to_s)] }.to_h
  SomeOtherObject.some_other_method(opts)
end

Think carefully about using this. It's clever but at the cost of readability, others reading your code won't like it.

You can make it slightly more readable with a helper method.

def params # Returns the parameters of the caller method.
  caller_method = caller_locations(length=1).first.label  
  method(caller_method).parameters 
end

def method(name: nil, color: nil, shoe_size: nil)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
  SomeOtherObject.some_other_method(opts)
end

Update: Ruby 2.2 introduced Binding#local_variables which can be used instead of Method#parameters. Be careful because you have to call local_variables before defining any additional local variables inside the method.

# Using Method#parameters
def foo(one: 1, two: 2, three: 3)
  params = method(__method__).parameters.map(&:last)
  opts = params.map { |p| [p, eval(p.to_s)] }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}

# Using Binding#local_variables (Ruby 2.2+)
def bar(one: 1, two: 2, three: 3)
  binding.local_variables.params.map { |p|
    [p, binding.local_variable_get(p)]
  }.to_h
end
#=> {:one=>1, :two=>2, :three=>3}