ruby eval scope

¿Es ''eval'' la única forma de interactuar con objetos vinculantes en Ruby?



scope (4)

Soy bastante nuevo para Ruby, y hasta ahora, descubrir cómo usar objetos "vinculantes" es uno de los mayores puntos de dolor para mí. Si estoy leyendo la documentación correctamente, son casi completamente opacos. Para acceder al alcance dentro del objeto vinculante, debe tener una cadena de código Ruby y evaluarlo utilizando el enlace.

Tal vez solo soy un purista de una escuela diferente, pero soy alérgico a las construcciones ''evalu'' basadas en cuerdas, en términos generales. ¿Hay alguna manera de hacer algo de lo siguiente, de forma segura y en el caso general, dado un objeto vinculante:

  1. Enumere los identificadores en el alcance en el contexto que representa el enlace, o recupere un hash de los contenidos.
  2. Establezca el valor de una variable local en el enlace igual a la de una variable local en un contexto externo. Idealmente, esto debería funcionar en general, incluso si el valor es una referencia de objeto, manejador de archivo o alguna otra entidad compleja.
  3. (extensión 2 :) Dado un hash, configure locales en el enlace para cada entrada.
  4. Mejor aún, dado un hash, compilar un enlace con solo las construcciones básicas del lenguaje y los nombres en el hash en el alcance.

Básicamente, quiero saber cuál es posible y cómo lograr los que sí lo son. Me imagino que las soluciones para cada uno estarán bastante relacionadas, por eso planteo todo esto en una sola pregunta.

Alternativamente, ¿hay alguna forma de evaluar el código que ya se ha analizado en el contexto de un enlace, similar a la sintaxis de eval BLOCK de Perl?


Al buscar más, encontré una respuesta a al menos parte de mi pregunta:

Basado en: http://wikis.onestepback.org/index.cgi/Tech/Ruby/RubyBindings.rdoc/style/print

El resto es de experimentación después de los útiles consejos de Jim Shubert.

  1. Esto se puede lograr local_variables - local_variables , instance_variables y global_variables dentro del enlace.
  2. Puede hacer algo como se describe a continuación, dado var_name , new_val , my_binding (la sintaxis puede ser imperfecta o mejorable, no dude en sugerir en los comentarios. Además, no pude obtener el formato del código para trabajar dentro de la lista, sugerencias de cómo hacerlo eso también será implementado.)
  3. Puedes tomar (2) y repetir el hash para hacer esto.
  4. Vea el segundo bloque de código a continuación. La idea es comenzar con TOPLEVEL_BINDING, que normalmente solo incluye las variables globales.

Esto implica el uso de string eval . Sin embargo, ningún valor de variable se expande nunca en las cadenas involucradas, por lo que debe ser bastante seguro si se usa como se describe, y debe trabajar para ''pasar'' valores complejos de variables.

También tenga en cuenta que siempre es posible hacer eval var_name, my_binding para obtener el valor de una variable. Tenga en cuenta que en todos estos usos es vital que el nombre de la variable sea seguro de evaluar, por lo que idealmente no debería provenir de ningún tipo de entrada del usuario.

Establecer una variable dentro de un enlace dado var_name , new_val , my_binding :

# the assignment to nil in the eval coerces the variable into existence at the outer scope setter = eval "#{var_name} = nil; lambda { |v| #{var_name} = v }", my_binding setter.call(new_val)

Construyendo un enlace "a medida":

my_binding = eval "lambda { binding }", TOPLEVEL_BINDING # build a nearly-empty binding # set_in_binding is based on the above snippet vars_to_include.each { |var_name, new_val| set_in_binding(var_name, new_val, my_binding) }


Walter, deberías poder interactuar directamente con el enlace. No he trabajado mucho con enlaces anteriormente, pero ejecuté un par de cosas en IRB:

jim@linux-g64g:~> irb irb(main):001:0> eval "self", TOPLEVEL_BINDING => main irb(main):002:0> eval "instance_variables", TOPLEVEL_BINDING => [] irb(main):003:0> eval "methods", TOPLEVEL_BINDING => ["irb_kill", "inspect", "chws", "install_alias_method", ....

También tengo Metaprogramación Ruby, que no habla mucho sobre la unión. Sin embargo, si toma esto, al final de la página 144 dice

En cierto sentido, puede ver los objetos de enlace como una forma "más pura" de cierres que los bloques, porque estos objetos contienen un alcance pero no contienen código.

Y, en la página opuesta, sugiere retocar el código de irb (eliminando los dos últimos argumentos de la llamada de evaluación) para ver cómo usa los enlaces:

// ctwc / irb / workspace.rb
eval (declaraciones, @binding) #, archivo, línea)

Y ... iba a sugerir que pasara el lambda, pero veo que acabas de responder, así que dejaré el irb tinkering como una sugerencia para futuras investigaciones.


¿Podrías explicar qué es exactamente lo que estás tratando de hacer? Proporcione algún código que muestre cómo desea que funcione. Puede haber una forma mejor y más segura de lograr lo que desea.

Voy a intentar adivinar su caso de uso típico. Dado un hash: {: a => 11,: b => 22}

Desea un entorno de ejecución mínimo, relativamente aislado, donde puede acceder a los valores del hash como variables locales. (No estoy seguro de por qué necesitarías que fueran locales, excepto tal vez si estás escribiendo un DSL, o si ya tienes un código escrito que no deseas adaptar para acceder al hash).

Aquí está mi intento. Para simplificar, asumo que solo usas símbolos como teclas Hash.

class MyBinding def initialize(varhash); @vars=varhash; end def method_missing(methname, *args) meth_s = methname.to_s if meth_s =~ /=/z/ @vars[meth_s.sub(/=/z/, '''').to_sym] = args.first else @vars[methname] end end def eval(&block) instance_eval &block end end

Uso de la muestra:

hash = {:a => 11, :b => 22} mb = MyBinding.new hash puts mb.eval { a + b } # setting values is not as natural: mb.eval { self.a = 33 } puts mb.eval { a + b }

Algunas advertencias:

1) No levanté NameError si la variable no existía, pero un reemplazo simple corrige que:

def initialize(varhash) @vars = Hash.new {|h,k| raise NameError, "undefined local variable or method `#{k}''" } @vars.update(varhash) end

2) Las reglas normales de alcance son tales que si existe un local cuyo nombre es igual a un método, el local tiene prioridad, a menos que realice explícitamente una llamada a un método, como a (). La clase anterior tiene el comportamiento opuesto; el método tiene prioridad. Para obtener un comportamiento "normal", deberá ocultar todos (o la mayoría) de los métodos estándar, como #object_id. ActiveSupport proporciona una clase BlankSlate para este propósito; solo mantiene 3 métodos:

__send__, instance_eval, __id__

. Para usarlo, simplemente haga que MyBinding herede de BlankSlate. Además de estos 3 métodos, tampoco podrás tener locales llamados "eval" o "method_missing".

3) Establecer un local no es tan natural, porque necesita "uno mismo" para recibir la llamada al método. No estoy seguro si hay una solución para esto.

4) El bloque eval puede meterse con el hash @vars.

5) Si tiene una var local real en el alcance donde llama a mb.eval, con el mismo nombre que una de las teclas hash, el local real tendrá prioridad ... esta es probablemente la desventaja más grande porque los errores sutiles pueden arrastrarse en.

Después de darme cuenta de las desventajas de mi técnica, recomiendo usar un Hash directamente para mantener un conjunto de variables, a menos que vea una razón diferente. Pero si aún desea utilizar la evaluación nativa, puede mejorar la seguridad mediante el uso de Regexps para evitar la inyección de código, y la configuración "local" de $ SAFE para ser mayor para la llamada de evaluación mediante el uso de un Proc, como sigue:

proc { $SAFE = 1; eval "do_some_stuff" }.call # returns the value of eval call


Aquí hay un código para una solución basada en Hash.

class ScopedHash def initialize(varhash) # You can use an OpenStruct instead of a Hash, but the you lose the NameError feature. # OpenStructs also don''t have the ability to list their members unless you call a protected method @vars = Hash.new {|h,k| raise NameError, "undefined local variable or method `#{k}''" } @vars.update(varhash) end def eval yield @vars end end if __FILE__ == $0 # sample usage hash = {:a => 11, :b => 22} sh = ScopedHash.new hash puts sh.eval {|v| v[:a] + v[:b] } sh.eval {|v| v[:a] = 33 } puts sh.eval {|v| v[:a] + v[:b] } sh.eval{|v| v[:c] } # raises NameError end

Entonces, en lugar de usar locales, simplemente accedería al Hash producido. Creo que hay muy pocas razones por las cuales uno se vería obligado a manipular un objeto Vinculante; generalmente hay formas más limpias de realizar la tarea.