Именованные аргументы как локальные переменные в Ruby

Я постоянно нахожу, что я вижу ненужный код в Ruby при использовании именованных аргументов для методов.

Возьмем, например, следующий код:

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"

То, что мне действительно не нравится в этом коде, заключается в том, что мне приходится принимать аргументы и передавать значения в локальные переменные, идущие против DRY-принципов и обычно занимая место в моих методах. И если я не использую локальные переменные и просто обращаюсь ко всем переменным с синтаксисом args [: symbol], тогда код становится несколько неразборчивым.

Я попытался разработать решение для этого, но не ударил кирпичную стену, так как не знаю, как определить локальные переменные, используя eval в рамках метода, или используя любую другую технику. Ниже приведена одна из многих попыток, которая приводит к ошибке

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"

При запуске этого кода я просто получаю

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

Кто-нибудь получил какие-либо идеи о том, как я мог бы упростить это так, чтобы мне не приходилось запускать мои методы с именами с нагрузками var = args [: var] code?

Спасибо, Мэтью О'Риордан

Ответ 1

Я не верю, что это можно сделать в Ruby (если кто-нибудь придумает, сообщите мне, и я обновлю или удалю этот ответ, чтобы отразить это!) - если локальная переменная hasn ' t еще не определено, нет возможности динамически определить его с привязкой. Возможно, вы могли бы сделать что-то вроде orange, lemon, grapefruit = nil перед вызовом eval, но вы можете столкнуться с другими проблемами - например, если args [: orange] - это строка "Оранжевый", вы в конечном итоге оцениваете orange = Orange своей текущей реализацией.

Вот что-то, что могло бы сработать, используя класс OpenStruct из стандартной библиотеки (по "может работать", я имею в виду "это зависит от вашего стиля, будь то a.orange лучше, чем 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

Если вам не нужен простой доступ к любому состоянию или методам получателя этого метода, вы можете использовать instance_eval, как показано ниже.

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

Вы даже можете сделать что-то сложное с method_missing (см. здесь для получения дополнительной информации), чтобы разрешить доступ к "первичному" объекту, но производительность, вероятно, будет невелика.

В целом, я думаю, что это, вероятно, наиболее просто/доступно для чтения с менее сухим исходным решением, которое беспокоило вас.

Ответ 2

Объяснение ответов Грега и Песка:

require 'ostruct'

def my_method(args = {}) 
  with args do
    puts a
    puts b
  end 
end

def with(args = {}, &block)
  OpenStruct.new(args).instance_eval(&block)
end

my_method(:a => 1, :b => 2)

Ответ 3

Я нашел обсуждение этого вопроса на ruby-talk-google и, похоже, это оптимизация синтаксического анализатора. Локальные переменные уже вычислены во время выполнения, так что local_variables уже задано в начале метода.

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

Таким образом, Ruby не нужно решать, является ли a метод или локальная переменная или что-то еще во время выполнения, но можно смело предположить, что это локальная переменная.

(Для сравнения: в Python locals() будет пустым в начале функции.)

Ответ 4

В моем блоге (см. ссылку в информации о пользователе) я просто попытался решить эту проблему аккуратно. Здесь я расскажу подробнее, но ядро ​​моего решения - это следующий вспомогательный метод:

def collect_named_args(given, expected)
    # collect any given arguments that were unexpected
    bad = given.keys - expected.keys

    # if we have any unexpected arguments, raise an exception.
    # Example error string: "unknown arguments sonething, anyhting"
    raise ArgumentError,
        "unknown argument#{bad.count > 1 ? 's' : ''}: #{bad.join(', ')}",
        caller unless bad.empty?

    Struct.new(*expected.keys).new(
        *expected.map { |arg, default_value| 
            given.has_key?(arg) ? given[arg] : default_value
        }
    )
end # def collect_named_args

который вызывается следующим образом:

def foo(arguments = {})
    a = collect_named_args(arguments,
        something:  'nothing',
        everything: 'almost',
        nothing:     false,
        anything:    75)

    # Do something with the arguments 
    puts a.anything
end # def foo

Я все еще пытаюсь выяснить, есть ли способ получить мои результаты в local_variables или нет, но, как отмечали другие, Ruby не хочет этого делать. Полагаю, вы могли бы использовать трюк "с".

module Kernel
  def with(object, &block)
    object.instance_eval &block
  end
end

затем

with(a) do
  # Do something with arguments (a)
  put anything
end

но это кажется неудовлетворительным по нескольким причинам.

Мне нравится вышеупомянутое решение, потому что оно использует Struct вместо OpenStruct, что означает, что нужно меньше требовать, а то, что вы получаете, задано до того, какие переменные обрабатываются.

Ответ 5

Это не решает проблему, но я стараюсь делать

orange, lemon, grapefruit = [:orange, :lemon, :grapefruit].
map{|key| args.fetch(key)}

так как довольно легко скопировать и вставить бит orange lemon grapefruit.

Если вы обнаружите, что двоеточия слишком много работают, вы можете сделать

orange, lemon, grapefruit = %w{orange, lemon, grapefruit}.
map{|str| str.gsub(",", "").to_sym}.map{|key| args.fetch(key)}

Ответ 6

Мне стало интересно, как это сделать сегодня. Мне не только понравился бы DRY мой код, но я бы тоже хотел иметь аргумент.

Я наткнулся на сообщение в блоге Juris Galang, где он объяснил пару способов обращения с ним. Он опубликовал драгоценный камень, который инкапсулирует его идеи, который выглядит интересным.