Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Named arguments as local variables in Ruby

I find myself constantly writing what I see as unnecessary code in Ruby when using named arguments for methods.

Take for example the following code:

def my_method(args)
  orange = args[:orange]
  lemon = args[:lemon]
  grapefruit = args[:grapefruit]

  # code that uses 
  # orange, lemon & grapefruit in this format which is way prettier & concise than 
  # args[:orange] args[:lemon] args[:grapefruit]

  puts "my_method variables: #{orange}, #{lemon}, #{grapefruit}" 
end
my_method :orange => "Orange", :grapefruit => "Grapefruit"

What I really don't like about this code is that I am having to take the args and pass the values into local variables going against DRY principles and just generally taking up space in my methods. And if I don't use local variables and just refer to all variables with the args[:symbol] syntax then the code becomes somewhat illegible.

I have tried working up a solution to this but keeping hitting a brick wall as I don't know how to define local variables using eval in the scope of the method, or using any other technique. Here is one of many attempts below, which results in an error

def my_method_with_eval(args)
  method_binding = binding
  %w{ orange lemon grapefruit}.each { |variable| eval "#{variable} = args[:#{variable}]", method_binding; }

  # code that uses 
  # orange, lemon & grapefruit in this format which is way prettier & concise than 
  # args[:orange] args[:lemon] args[:grapefruit]

  puts "my_method_with_eval variables: #{orange}, #{lemon}, #{grapefruit}" 
end
my_method_with_eval :orange => "Orange", :grapefruit => "Grapefruit"

When running that code I simply get

NameError: undefined local variable or method ‘orange’ for main:Object method my_method_with_eval in named_args_to_local_vars at line at top level in named_args_to_local_vars at line 9

Anyone got any ideas how I could simplify this down somehow so that I don't have to start my named argument methods with loads of var=args[:var] code?

Thanks, Matthew O'Riordan

like image 819
Matthew O'Riordan Avatar asked Jan 07 '10 23:01

Matthew O'Riordan


People also ask

How do you pass a named parameter in Ruby?

You can define the default value and the name of the parameter and then call the method the way you would call it if you had hash-based "named" parameters but without the need to define defaults in your method. You would need this in your method for each "named parameter" if you were using a hash. Save this answer.

How do you define a local variable in Ruby?

Local Variables: A local variable name always starts with a lowercase letter(a-z) or underscore (_). These variables are local to the code construct in which they are declared. A local variable is only accessible within the block of its initialization. Local variables are not available outside the method.

How does * args work 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).

Which variable name is valid in Ruby?

Variable names in Ruby can be created from alphanumeric characters and the underscore _ character. A variable cannot begin with a number. This makes it easier for the interpreter to distinguish a literal number from a variable. Variable names cannot begin with a capital letter.


1 Answers

I don't believe there's any way to do this in Ruby (if anyone comes up with one, please let me know, and I'll update or delete this answer to reflect it!) - if a local variable hasn't been defined yet, there's no way to dynamically define it with the binding. You could conceivably do something like orange, lemon, grapefruit = nil before calling eval, but you may run into other problems - for instance, if args[:orange] is the string "Orange", you'll end up evaluating orange = Orange with your current implementation.

Here's something that could work, though, using the OpenStruct class from the standard library (by "could work", I mean "it's up to your sense of style whether a.orange is any nicer than args[:orange]"):

require 'ostruct'

def my_method_with_ostruct(args)
  a = OpenStruct.new(args)
  puts "my_method_with_ostruct variables: #{a.orange}, #{a.lemon}, #{a.grapefruit}"
end

If you don't need easy access to any state or methods on the receiver of this method, you could use instance_eval, as follows.

def my_method_with_instance_eval(args)
  OpenStruct.new(args).instance_eval do
    puts "my_method_with_instance_eval variables: #{orange}, #{lemon}, #{grapefruit}"
  end
end

You could even do something tricky with method_missing (see here for more) to allow access to the "primary" object, but the performance probably wouldn't be great.

All in all, I think it's probably most straightforward/readable to go with the less DRY initial solution that bothered you.

like image 158
Greg Campbell Avatar answered Sep 21 '22 16:09

Greg Campbell