sábado, 29 de mayo de 2010

Definicion de clases y metodos en un eval

Una de las miles de cualidades que tiene ruby como lenguaje dinamico es la ejecucion de codigo ruby contenido en un string mediante el uso de la funcion eval.
Asi se pueden hacer cosas bastante interesantes ejecutando codigo ingresado dinamicamente ^_^
No obstante es necesario conocer como sortear determinados obstaculos en la ejecucion de codigo dinamico


class Runner
def run(code)
eval(code)
end
end

runner = Runner.new

# ¡¡FAIL!! :(, no se puede definir classes desde adentro de un metodo (Runner#run)
runner.run("
class X
def foo
print \"hello world\n\"
end
end

x = X.new
x.foo
")



Lo que se intento hacer en el ejemplo anterior, para el interprete es equivalente a:


class Runner
def run(code)
# いけない!!!
# en ruby no esta permitido definir clases adentro de metodos
# (aunque si permite hacer otras cosas locas XD )
class X
def foo
print \"hello world\n\"
end
end

x = X.new
x.foo
end
end


Para poder hacerlo correctamente hay que pasarle un segundo parametro a eval que es el "binding", un binding representa el contexto en ruby de donde se llama el binding, que incluye las variables locales, etc... mejor verlo en el ejemplo que explicarlo:


class Runner

class << self
attr_accessor :runner_binding
end

def run(code)
# se especifica un binding al eval para definir el contexto
# en el que se ejecuta el codigo
eval(code, Runner.runner_binding)
end
end

# se copia el binding del contexto actual (fuera de cualquier clase o metodo)
Runner.runner_binding = binding

runner = Runner.new

# se puede definir clases en el codigo pasado a eval, porque el binding
# esta afuera de cualquier declaracion de clase o metodo y el eval se
# llama con ese binding
runner.run("
class X
def foo
print \"hello world\n\"
end
end

x = X.new
x.foo
")




Que para el interprete es lo mismo que reemplazar el codigo en donde se llama a binding en lugar de en donde se llama a eval:


class Runner

class << self
attr_accessor :runner_binding
end

def run(code)
# se especifica un binding al eval para definir el contexto
# en el que se ejecuta el codigo
eval(code, Runner.runner_binding)
end
end

# よろしい
# Runner.runner_binding = binding
class X
def foo
print \"hello world\n\"
end
end

x = X.new
x.foo




Y ya que estamos, les cuento que otra cosa tiene el eval para hacer


def bar
1/0 # ZeroDivisionError
end

def foo(code) # ¿porque siempre foo?
eval(code)
end

foo("bar") #


Resultado:


test.rb:6:in `foo': test.rb:2:in `/': divided by 0 (ZeroDivisionError)
from test.rb:2:in `bar'
from (eval):1:in `foo'
from test.rb:9:in `eval'
from test.rb:6:in `foo'
from test.rb:9



Mejor, pasarle unos argumentos adicionales a eval especificando el "archivo" y el numero de linea de donde viene el codigo evaluado


def bar
1/0 # ZeroDivisionError
end

def foo(code) # ¿porque siempre foo?
eval(code, binding, "foo_eval.rb", 1)
end

foo("bar") #


Resultado:


test.rb:2:in `/': divided by 0 (ZeroDivisionError)
from test.rb:2:in `bar'
from foo_eval.rb:1:in `foo'
from test.rb:6:in `foo'
from test.rb:9


Ahora se puede ver mejor en el backtrace de donde viene el codigo, con esto mejorada la trazabilidad cuando se usa eval.

No hay comentarios: