Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 954
Jesse Farmer Avatar asked Feb 25 '14 21:02

Jesse Farmer


People also ask

How do you use keyword arguments in Ruby?

If you want to accept keyword arguments, in principle you should always use def foo(k: default) or def foo(k:) or def foo(**kwargs) . Note that Ruby 3.0 doesn't behave differently when calling a method which doesn't accept keyword arguments with keyword arguments.

How are the arguments in Ruby passed?

In ruby, arguments inside a method are passed by reference In ruby, we have a different situation, the variable that we have inside the method stores a reference to an object. Thus, if we will change an object inside the method, then it will be changed also outside the method.

What does * args mean in Ruby?

In the code you posted, *args simply indicates that the method accepts a variable number of arguments in an array called args . It could have been called anything you want (following the Ruby naming rules, of course).

Can we pass keyword arguments in any order?

A keyword argument is a name-value pair that is passed to the function. Here are some of the advantages: If the values passed with positional arguments are wrong, you will get an error or unexpected behavior. Keyword arguments can be passed in any order.


1 Answers

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} 
like image 73
Dennis Avatar answered Sep 18 '22 13:09

Dennis