Consideremos el siguiente ejemplo
x = 10
class A
puts x
end
# NameError: undefined local variable or method `x' for A:Class
def m
puts x
end
m # NameError: undefined local variable or method `x' for main:Object
Esto pasa porque la variable ‘x’ solo puede ser accedida desde el contexto donde se creó. El contexto cambia en tres casos (se los suele llamar scope gates):
Una forma de saltear la restricción de los cambios de contexto, es usar closures. Ruby tiene tres tipos de closures: los bloques, lambdas y procs. Su principal característica es que “recuerdan” el contexto donde fueron creados.
a = 5
p = lambda {a = a + 1}
p.call # 6
p.call # 7
a # 7
Para hacer que una variable esté en el contexto de la definición de un método, podemos reemplazar def con define_method.
x = 10
define_method(:m) do
x + 5
end
m # 15
De la misma manera, podemos superar el scope gate que define class reemplazandolo con Class.new
x = 10
una_clase = Class.new do
x += 5
end
x # 15
A esta técnica que permite mantener el mismo contexto se la llama flat scope (contexto aplanado).
Las lambdas y los procs son objetos que reifican comportamiento y pueden ser ejecutados diferidamente. Tienen dos diferencias fundamentales:
lam = lambda { |x| puts x } # creates a lambda that takes 1 argument
lam.call(2) # prints out 2
lam.call # ArgumentError: wrong number of arguments (0 for 1)
lam.call(1,2,3) # ArgumentError: wrong number of arguments (3 for 1)
proc = Proc.new { |x| puts x } # creates a proc that takes 1 argument
proc.call(2) # prints out 2
proc.call # returns nil
proc.call(1,2,3) # prints out 1 and forgets about the extra arguments
---------------------------------------------------------------------------------------------
def lambda_test
lam = lambda { return }
lam.call
puts "Hello world"
end
lambda_test # calling lambda_test prints 'Hello World'
def proc_test
proc = Proc.new { return }
proc.call
puts "Hello world"
end
proc_test # calling proc_test prints nothing
Los bloques (tanto si los escribimos con llaves como con do y end) no son objetos y sólo se puede pasar un bloque como último parámetro del método.
def bloque_test
yield(3)
end
bloque_test do |x|
x + 2
end
Cuando sea necesario, se puede pasar un proc en lugar de un bloque usando &
def bloque_proc_test(&bloque)
bloque.call(3)
end
bloque_test do |x|
x + 2
end
Estamos acostumbrados a que, dentro de un método de una clase, podemos mandar un mensaje al objeto actual sin definir ningún receptor. Por ejemplo:
class Usuario
attr_accessor :edad
def initialize(edad)
@edad = edad
end
def mayor_de_edad?
edad >= 18
end
end
Usuario.new(19).mayor_de_edad?
# true
Para mandar el mensaje edad
en el método mayor_de_edad?
no necesitamos poner self.edad
ya que self
es el contexto implícito.
¿Cuál es el contexto dentro de un bloque? Los mensajes dentro de un bloque tienen receptor implícito?
class Usuario
def lazy_edad
proc { edad }
end
end
Usuario.new(19).lazy_edad.call
# 19
¿Y si ese bloque lo invoco dentro de otro usuario?
class Usuario
def con_bloque(bloque)
bloque.call
end
end
mayor = Usuario.new(19)
menor = Usuario.new(15)
menor.con_bloque(mayor.lazy_edad)
# 19
El contexto implícito de un bloque es el mismo que el que existía al momento de ser creado. Si necesito que el bloque mande mensajes a otro usuario, voy a tener que pasarlo por parámetro:
class Usuario
def edad_de
proc { |u| u.edad }
end
end
Usuario.new(19).edad_de.call(Usuario.new(15))
# 15
Si pudiera cambiar el receptor default de los bloques, podría evitar pasar por parámetros el destino de mis mensajes!
class Usuario
def edad_de
proc { edad }
end
end
bloque = menor.edad_de
mayor.instance_eval(&bloque)
# 19
menor.instance_eval(&bloque)
# 15
instance_eval
: cambiar el contexto de un bloque sin parámetrosinstance_exec
: cambiar el contexto pudiendo pasar parámetros al bloqueCuando creamos un método dentro del bloque usando def
:
instance_eval
: lo define en la singleton class del objetomodule_eval
, class_eval
: lo agrega como parte de los instance methods de la clase o el modulo.Usando el código del Peloton, definamos estrategias con bloques sin pasarle ningún parámetro.
def self.cobarde(integrantes)
new(integrantes) { retirate }
end
def self.descansador(integrantes)
new(integrantes) { descansar }
end
def lastimado
instance_eval(&estrategia)
end
Ya tenemos dos factory methods para construir distintos tipos de Peloton: descansador y cobarde. Queremos tener una forma de definir dinámicamente métodos similares con distintas estrategias. Uso:
Peloton.definir :descansador_cobarde do
descansar
retirate
end
Tiene que definir un método de clase en Peloton:
un_peloton = Peloton.descansador_cobarde integrantes
def self.definir(nombre, &estrategia)
self.define_singleton_method nombre do |integrantes|
self.new(integrantes, &estrategia)
end
end
En este caso self es la clase Peloton, por lo que define_singleton_method va a definir un método en la singleton class de Peloton(#Peloton).
Por un momento recordemos el patrón decorator. Queremos construir un decorator que retorne retorne “Anon” para los nombres de las personas pero que siga pudiendo acceder al resto de su comportamiento.
La forma clásica de solucionar este problema es redefinir todos los mensajes que entiende la persona en el objeto decorador. Aquellos que tienen que mantener la lógica del objeto de origen, solo se pasa el mensaje hacia él. En los mensajes que deben cambiar de comportamiento se agrega la nueva lógica.
Sin embargo, ésta lógica repite mucho código y es muy fragil (si la persona modifica los mensajes que entiende, el decorador debe ser adaptado también).
Si pudiéramos capturar todos los mensajes que se envían a un objeto podríamos generalizar esa lógica sin repetir código. Ruby (y otros lenguajes) nos permite hacer esto mediante el mensaje “method_missing”.
class Persona
attr_accessor :nombre, :edad
def initialize(nombre, edad)
@nombre = nombre
@edad = edad
end
end
class Anonimo
def initialize(persona)
@persona = persona
end
def method_missing(symbol, *args, &block)
@persona.send(symbol, *args, &block)
end
def nombre
"Anon"
end
end
p = Persona.new("Pablo", 32)
p.nombre
# Pablo
ap = Anonimo.new(p)
ap.nombre
# Anon
ap.edad
# 32
Ahora quiero poder tener estos métodos pero usando una convención.
Quiero que la clase Peloton entienda los mensajes con la siguiente forma: estrategia_<nombre de mensaje>
. Al enviarlo, va a retornar un pelotón que se enviará a si mismo el mensaje <nombre de mensaje>
al ser lastimado.
def self.method_missing(symbol, *args, &block)
if (symbol.to_s.start_with?('estrategia_') && args.length == 1)
# mejorar error si lo invoco con diferente cantidad de parámetros
message = symbol.to_s.gsub('estrategia_', '').to_sym
Peloton.new(args[0]) {
send(message)
}
else
super
end
end
Redefinir method_missing tiene un efecto no deseado, Peloton ahora entiende mensajes pero respond_to? de los mismos retorna false. Para mitigar este problema, el contrato cuando se redefine method_missing es que hay que redefinir respond_to_missing?
def self.respond_to_missing?(sym, priv = false)
sym.to_s.start_with?('estrategia_')
end
Para definir constantes en ruby, se puede usar Class»const_set
class Guerrero
end
Object.const_set :Atila, Guerrero.new
Atila.atacar(otro)
Sólo las clases pueden definir constantes y solo viven en el scope de la clase que la definió.
class Bla
def m
A
end
end
Bla.const_set :A, "Bla::A"
Object.const_set :A, "Object::A"
bla = Bla.new
bla.m # "Bla::A"
A # "Object::A"
class Bla
A # "Bla::A"
end
Bla.class_eval do
A # "Object::A"
end
También existe el método const_missing, que al igual que const_set, lo entienden sólo las clases. Cumple la misma función que method_missing, pero para cuando no se encuentra una constante.
class Bla
def self.const_missing const
"#{const} no encontrada"
end
end
Bla::T # "T no encontrada"
Ruby provee un mecanismo para “avisar” cuando se agrega un método en una clase:
class A
def self.method_added(method_name)
puts "Se agregó el método #{method_name}"
end
end
class A
def un_metodo
# ...
end
end
# 'Se agregó el método un_metodo'
También se puede saber si se agregó un singleton method:
class A
def singleton_method_added(name)
puts "Singleton added #{name}"
end
end
a = A.new
def a.m
puts 'Soy un a'
end
# 'Singleton added m'