Именованные аргументы как локальные переменные в Ruby - PullRequest
10 голосов
/ 08 января 2010

Я постоянно пишу то, что я считаю ненужным кодом в 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]?

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

Ответы [ 6 ]

6 голосов
/ 16 марта 2011

Слияние ответов Грега и Сэнд:

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)
6 голосов
/ 08 января 2010

Я не верю, что есть какой-либо способ сделать это в Ruby (если кто-нибудь придет с ним, пожалуйста, дайте мне знать, и я обновлю или удалю этот ответ, чтобы отразить его!) - если локальная переменная не имеет ' Пока не определено, нет способа динамически определить его с помощью привязки. Вы могли бы предположительно сделать что-то вроде orange, lemon, grapefruit = nil перед вызовом eval, но вы можете столкнуться с другими проблемами - например, если args [: orange] является строкой «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 (подробнее см. здесь ), чтобы разрешить доступ к "первичному" объекту, но производительность, вероятно, будет не очень хорошей.

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

3 голосов
/ 08 января 2010

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

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

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

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

1 голос
/ 16 марта 2011

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

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

1010 * тогда *

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

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

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

0 голосов
/ 28 мая 2011

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

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

0 голосов
/ 17 марта 2011

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

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)}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...