views:

334

answers:

2

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

+5  A: 

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.

Greg Campbell
Thanks for the input. After still a few days more of trying to find a solution, I think your suggestion to keep it much like it is now is probably best! Trying to add variables to local_variables seems to be incredibly difficult and just not something Ruby encourages. I did see that there used to be an extension Binding.caller (in http://extensions.rubyforge.org/rdoc/index.html) which would have provided functionality to get the calling binding and insert local variables, but that has been deprecated.
+3  A: 

I found a discussion on this on ruby-talk-google and it seems to be an optimisation of the parser. Local variables are already figured out at runtime so that local_variables is already set at the beginning of the method.

def meth
  p local_variables
  a = 0
  p local_variables
end
meth
# =>
[:a]
[:a]

That way Ruby doesn’t need to decide whether a is a method or a local variable or whatnot at runtime but can safely assume it is a local variable.

(For comparison: In Python locals() would be empty at the beginning of the function.)

Debilski
Lots of good information in that thread - good find.
Greg Campbell