2010-01-07 10 views
10

Me encuentro escribiendo constantemente lo que veo como código innecesario en Ruby cuando uso argumentos con nombre para los métodos.Argumentos con nombre como variables locales en Ruby

Tomemos como ejemplo el siguiente código:

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" 

Lo que realmente no me gusta de este código es que estoy teniendo que tomar los argumentos y pasar los valores en variables locales ir en contra de los principios seco y justo generalmente ocupando espacio en mis métodos. Y si no uso las variables locales y solo me refiero a todas las variables con la sintaxis de args [: symbol], el código se vuelve algo ilegible.

He intentado encontrar una solución a esto pero seguir golpeando una pared de ladrillo ya que no sé cómo definir variables locales usando eval en el alcance del método, o usando cualquier otra técnica. Esto es uno de los muchos intentos por debajo, lo que resulta en un 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" 

Cuando se ejecuta el código que simplemente obtener

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

Alguien tiene alguna idea de cómo podría simplificar esta abajo de alguna manera para que yo no es necesario que inicie mis métodos de argumentos nombrados con cargas de código var = args [: var]?

Gracias, Matthew O'Riordan

+0

Parece que lo que realmente quiere aquí se llama argumentos. Esos existen en Ruby 2.0. – Ajedi32

Respuesta

6

No creo que haya ninguna manera de hacer esto en Rubí (si alguien se le ocurre una, por favor hágamelo saber y voy a actualizar o borrar esta respuesta para reflejarlo !): si aún no se ha definido una variable local, no hay forma de definirla dinámicamente con el enlace. Posiblemente podría hacer algo como orange, lemon, grapefruit = nil antes de llamar a eval, pero puede tener otros problemas, por ejemplo, si args [: orange] es la cadena "Naranja", terminará evaluando orange = Orange con su implementación actual.

Aquí hay algo que podría funcionar, sin embargo, el uso de la clase OpenStruct de la librería estándar (por "podría funcionar", quiero decir "que depende de su sentido del estilo si a.orange es más amable que 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 

Si no necesita acceder fácilmente a ningún estado o método en el receptor de este método, puede usar instance_eval, de la siguiente manera.

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 

Incluso se puede hacer something tricky with method_missing (ver here para más) para permitir el acceso al objeto "primario", pero el rendimiento probablemente no será grande.

Con todo, creo que es probablemente más sencillo/fácil de leer con la solución inicial menos DRY que le molestó.

+0

Gracias por la entrada. ¡Después de unos días más de tratar de encontrar una solución, creo que su sugerencia de mantenerla como ahora es probablemente la mejor! Intentar agregar variables a local_variables parece ser increíblemente difícil y no es algo que Ruby aliente. Vi que solía haber una extensión Binding.caller (en http://extensions.rubyforge.org/rdoc/index.html) que habría proporcionado la funcionalidad para obtener el enlace de llamadas e insertar variables locales, pero eso ha sido obsoleto. –

3

Encontré una discusión sobre esto en ruby-talk-google y parece ser una optimización del analizador. Las variables locales ya están resueltas en el tiempo de ejecución, por lo que local_variables ya está establecido al comienzo del método.

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

De esa manera Rubí no tiene que decidir si a es un método o una variable local o lo que sea en tiempo de ejecución, pero se puede suponer con seguridad que es una variable local.

(Para la comparación:. En Python locals() sería vacía al principio de la función)

+0

Mucha información buena en ese hilo - buen hallazgo. –

1

En mi blog (ver enlace en la información del usuario), que acaba de intentar abordar el manejo de este problema perfectamente. Me entrar en más detalles, pero el núcleo de mi solución es el siguiente método de ayuda:

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 

que se llama como sigue:

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 

Todavía estoy tratando de averiguar si hay cualquier forma de obtener mis resultados en variables locales o no, pero como otros han notado, Ruby no quiere hacer eso. Podría usar el truco "con", supongo.

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

continuación

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

pero que se siente insatisfactoria por varias razones.

Me gusta la solución anterior porque utiliza un Struct en lugar de un OpenStruct, lo que significa que requiere menos, y lo que obtiene se establece en cuanto a qué variables se están manejando.

6

Combinar de Greg y respuestas de arena:

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) 
+0

Esto parece una solución bastante ingeniosa y potencialmente muy útil – RubyFanatic

+0

Método hermoso y elegante. Funciona genial. ¡Gracias por compartir! – plang

0

esto no resuelve el problema, sino que tienden a hacer

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

ya que es bastante fácil de copiar y pegar el bit orange lemon grapefruit.

Si usted encuentra los dos puntos demasiado trabajo, que podría hacer

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

me encontré preguntándome cómo hacer esto a mí mismo hoy. No solo me gustaría SECAR mi código, sino que también me gustaría tener validación de argumentos.

Me encontré con a blog post by Juris Galang donde se le explicaron algunas maneras de manejarlo. Ha publicado a gem that encapsulates his ideas que parece interesante.

Cuestiones relacionadas