Es el proceso o la práctica por la cual escribimos programas que generan, manipulan o utilizan otros programas.
En general la metaprogramación se utiliza más fuertemente en el desarrollo de frameworks y herramientas.
Dado que los frameworks van a resolver cierta problemática de las aplicaciones, no van a estar diseñados para ninguna en particular. Es decir, la idea de framework es que se va a poder aplicar y utilizar en diferentes dominios desconocidos para el creador del framework.
Ejemplos:
Es un caso particular de metaprogramación, donde “metaprogramamos” en el mismo lenguaje en que están escritos (o vamos a escribir) los programas. Es decir, todo desde el mismo lenguaje.
Para esto, generalmente, es necesario contar con facilidades o herramientas específicas, digamos “soporte” del lenguaje. Entonces reflection, además, abarca los siguientes items que vamos a mencionar en esta lista:
Así como todo programa construye un modelo para describir su dominio, los lenguajes pueden hacer lo mismo para describir sus abstracciones. El dominio de un metaprograma son los programas.
El programa describe las características de los elementos del dominio utilizando clases, métodos, atributos entre otros. Entonces, el modelo puede contener por ejemplo una clase Guerrero, que modela a los guerreros en el domino.
Un metaprograma usará el metamodelo que describe al programa base. Así como en el dominio hay guerreros, los elementos del “metadominio” serán las construcciones del lenguaje. Por ejemplo: clases, atributos, métodos.
Vamos a usar el código de la clase anterior como ejemplo: https://github.com/tadp-utn-frba/tadp-clases/tree/ruby-age
Podemos comenzar con un poco de introspection y preguntarle a un objeto desde su clase hasta que metodos tiene.
require_relative 'age'
atila = Guerrero.new
atila.class #=> Guerrero
atila.class.superclass #=> Object
atila.methods #=> [:sufri_danio, :descansar, :energia, :energia=,...]
Guerrero.instance_methods #=> Idem a atila.methods
Guerrero.instance_methods(false) #=> [:descansar_atacante, :descansar_defensor,...] (solo los de la clase guerrero)
Tambien podemos empezar a interactuar con los objetos de otra manera, como mandarle mensajes de otra manera.
atila.send(:potencial_ofensivo) #=> 20
atila.send(:descansar) #=> 110
# con send no existen los metodos privados, la seguridad es una sensacion
class A
private
def metodo_privado
'cosa privada, no te metas'
end
end
objeto = A.new
objeto.metodo_privado #=> NoMethodError: private method `metodo_privado' called for #<A:direccion en memoria del objeto>
objeto.send(:metodo_privado) #=> "cosa privada, no te metas"
Tambien podemos obtener un metodo e invocarlo
metodo = atila.method(:potencial_ofensivo) #=> #<Method: Guerrero(Atacante)#potencial_ofensivo>
metodo.call #=> 20
Algo interesante que podemos hacer es pedirle un metodo de instancia a una clase. A este metodo se lo llama Unbound Method, ya que no esta asociado a ninguna instancia de esa clase. Podemos asociarlo a un objeto siempre y cuando este dentro de la jerarquia de clases.
class Padre
def correr
'correr como padre'
end
end
class Hijo < Padre
def correr
'correr como hijo'
end
end
metodo = Padre.instance_method(:correr) #=>#<UnboundMethod: Padre#correr>
metodo.bind(Hijo.new).call #=> 'correr como padre'
Como vemos los Unbound Methods se escapan al metodo lookup. Tambien podemos preguntarle cosas a los metodos.
metodo = atila.method(:atacar) #=> #<Method: Guerrero(Atacante)#atacar>
metodo.arity #=> 1
metodo.parameters #=> [[:req, :un_defensor]]
metodo.owner #=> Atacante (donde esta definido)
Asi como ya estuvimos jugando con las clases, objetos y metodos, tambien podemos jugar con las variables.
atila.instance_variables #=> [:@potencial_ofensivo, :@energia, :@potencial_defensivo]
atila.instance_variable_get(:@energia) #=> 100
atila.instance_variable_set(:@energia, 50) #=> 50
atila.instance_variable_get(:@energia) #=> 50
atila #=> pretty print muestra el estado interno
Nos permite definir métodos y atributos en una clase ya existente. Es una forma de self modification con azucar sintáctica para no tener que hacerlo mediante mensajes.
Los métodos que se definan con el mismo nombre que otro ya existente, serán reemplazados (es destructivo).
Para Ruby, las firmas de los métodos están definidas solo por el nombre. Un método con el mismo nombre y diferente cantidad de parametros definen el mismo método.
class String
def importante
self + '!'
end
end
'aprobe'.importante #=> "aprobe!"
#Cambiar métodos
class Fixnum
def +(x)
123
end
end
2+2 #=> 123
Otra manera de abrir las clases y definir metodos es usando en la clase el define_method
pero este es privado y como ya vimos podemos pasarlo por arriba invocando el send.
Guerrero.send(:define_method, :saluda) {
'Hola'
}
Guerrero.new.saluda #=> "Hola"
Aca podemos hacer referencia a dos practicas de programacion: Duck Typing y Monkey Patching.
Debido a que ruby es un lenguaje dinamicamente tipado, hacemos referencia a un tipo de dato por el comportamiento que tiene.
…if it walks like a duck and talks like a duck, it’s a duck, right?
Si tenemos un objeto que cuando hace ruido hace “cuak” y camina como un pato, probablemente lo sea, y deberia poder continuar usando este objeto como si fuera uno.
…if it walks like a monkey and talks like a monkey, it’s a monkey, right? So if this monkey is not giving you the noise that you want, you’ve got to just punch that monkey until it returns what you expect.
Hace referencia a la posibilidad de practicamente modificar un tipo a gusto y piacere para que responda a nuestras necesidades y realizar otro tipo de operaciones como si fuera otro.
Tambien podemos empezar a hacer algunas cosas mas locas, como agregarle comportamiento a un unico objeto
atila.define_singleton_method(:saluda) {
'Hola soy Atila'
}
atila.saluda #=> "Hola soy Atila"
Guerrero.new.saluda # NoMethodError
Empecemos a descubrir el modelo de clases.
Vamos a jugar un poco más con el metamodelo, ya sabemos que existe el mensaje class
que lo entienden todos los objetos, si queremos saber la superclase de una clase tenemos el mensaje superclass
. Podríamos pensar en base a eso quiénes le proveen comportamiento a cada uno de nuestros objetos.
zorro = Espadachin.new(Espada.new(123))
Tenemos al zorro que es instancia de Espadachin, que hereda de Guerrero. Si le pedimos los métodos al zorro vemos que incluye a los instance_methods
de Espadachin y de Guerrero, así como todos los que se definen para las instancias de Object.
Pero nosotros no le agregamos comportamiento sólo a las instancias, también teníamos un par de métodos de clase que habíamos definido para Peloton, como por ejemplo cobarde.
Peloton.cobarde([])
Quién provee ese comportamiento? La clase de Peloton es Class, y este comportamiento no se agregó para todas las clases así que no puede estar definido dentro de Class.
Peloton.methods.include? :cobarde #=> true
Peloton.class.instance_methods.include? :new #=> true
Peloton.class.instance_methods.include? :cobarde #=> false
Esto sólo lo entiende la clase Peloton, o sea que está definido para un sólo objeto. El objeto que le provee el comportamiento a un sólo objeto es la autoclase. En Ruby podemos obtener la autoclase de un objeto mandándole singleton_class
.
Peloton.singleton_class.instance_methods(false) #=> [:cobarde, :descansador]
Todos los objetos tienen una singleton class, con lo cual podemos definirle comportamiento a atila y que sea el único guerrero con ese comportamiento.
Incluyanmos un mixin a una instancia, utilizando include en la singleton class o extend en la intancia:
module W
def m
123
end
end
a = Guerrero.new
a.singleton_class.include W
a.m #=> 123
b = Guerrero.new
b.extend W
b.m #=>123
Agreguemos un test en el que atila cuando se lastima descansa y se come un pollo, incorporando comerse_un_pollo
:
atila = Guerrero.new
atila.singleton_class.send(:define_method, :comerse_un_pollo, proc { @energia += 20 })
atila.energia
atila.comerse_un_pollo
atila.energia
Guerrero.new.comerse_un_pollo # NoMethodError
También vemos que se puede tener properties para un único objeto:
atila.singleton_class.send(:attr_accessor, :edad)
atila.edad = 5
atila.edad #=> 5
Guerrero.new.edad #=> NoMethodError
Vemos que no aparecen los mixins que tiene la clase Guerrero si vamos preguntando las super clases. El mensaje superclass
no muestra los mixins (porque no son clases), para poder ver la linearización tenemos que pedir quienes proveen comportamiento con el mensaje ancestors.
Guerrero.ancestors #=> [Guerrero, Defensor, Atacante, Object, PP::ObjectMixin, Kernel, BasicObject]
Las últimas versiones de ruby incluyen a la singleton class de Guerrero, las anteriores a 2.1.0 NO incluyen a las singleton classes:
Guerrero.new.singleton_class.ancestors
[#<Class:#<Guerrero:0x00000001d6c8c8>>,
Guerrero,
Defensor,
Atacante,
Object,
PP::ObjectMixin,
Kernel,
BasicObject]
Dibujar los ancestors de Guerrero en el pizarron. Dibujar la singleton class de atila. Agregamos un método de clase a Guerrero:
class Guerrero
def self.gritar
'haaaa'
end
end
atila.gritar #=> NoMethodError
Guerrero.gritar #=> haaaa
Veamos en el diagrama, la singleton class de Guerrero (donde definimos gritar
)
Espadachin.gritar #=>haaaa