O Metaprogramación, en castellano.
Voy a tomar un post muy bueno sobre metaprogramación en Ruby. Empecemos definiendo la metaprogramación, que es? código que escribe código, y listo.
Como pueden sospechar no es una característica solo de Ruby, en realidad es de muchos lenguajes. Sin embargo es mucho más fácil, hacer metaprogramación en lenguajes dinámicos e interpretados. Si no fuéramos puristas podríamos pensar que por ejemplo un Proxy de spring en Java es metaprogramación, ya que spring genera una clase al vuelo (en tiempo de ejecución) y la implementa. Por lo tanto genera código, por lo tanto es metaprogramación.
Otra virtud necesaria en lenguaje para que la metaprogramación sea útil o sea más útil, es que el lenguaje sea reflexivo o se pueda inspeccionar los objetos, en castellano, los objetos pueden decirte si responden mensajes o que atributos tiene, etc. El etc quiere decir que podemos modificar algunos métodos o hacer macana como romper el encapsulamiento pero recuerda:
“With great power comes great responsibility.”
Pero vuelvo a Ruby y para esto voy a copiar un poco de código. jeje!
Vamos a hacer un método que limpie una cadena, limpie la cadena de caracteres extraños:
def to_alphanumeric(s)
s.gsub(/[^\w\s]/, '')
end
puts to_alphanumeric("A&^ar$o%n&* (is&*&))) t&*(*he B0&*S**^S)")
# => "Aaron is the B0SS"
En Ruby tenemos clases abiertas (como smalltalk) por lo que podemos poner este método en la clase String.
class String
def to_alphanumeric
gsub(/[^\w\s]/, '')
end
end
puts "A&^ar$o%n&* (is&*&))) t&*(*he B0&*S**^S)".to_alphanumeric
# => "Aaron is the B0SS"
Las clases abiertas no son la panacea, podemos hacer desastre. Podemos reescribir métodos de clases propias de ruby. Esto se llama
Monkeypatching.
Modelo de Objetos
Antes de seguir debemos ver la jerarquia de clases de Ruby:
Parece complejo, pero no lo es, veamos los detalles:
- Tenemos 3 instancias de la clase MyClass (obj1, obj2, obj3)
- MyClass tiene una clase Class, esto es porque las clases tambien son objetos.
- Los objetos tienen una clase que es MyClass.
Cadena de ancestros
Bueno, recuerden esto porque ahora nos vamos a analizar, la herencia en especial la cadena de ancestros en ingles Ancestors Chain.
Este diagrama es fácil de entender:
Cuando se llama a un método, Ruby va directamente a la clase del receptor (en este caso libro) y luego en cadena a los antepasados, hasta que cualquiera encuentre el método o llegue al final de la cadena.
En este diagrama, un objeto b es creado de una instancia de la clase libro. Libro tiene 2 módulos: imprimir y documentos. Libro hereda de la clase Object, que es la clase que casi todos heredan en Ruby. Objeto incluye un módulo llamado Kernel. Y, por último, el objeto hereda de BasicObject - el padre absoluto de cada objeto en Ruby.
Ya aburrí muchísimo con la introducción, pero en esta parte deberían tener claro el modelo de objetos y la cadena de ancestros. Si no lo tienen en claro bueno, deberían volver a leerlo.
Métodos
En Ruby podemos crear métodos de forma dinámica y a la vez llamar método de forma dinámica, tambien. Vamos por parte, primero ver crear métodos de forma dinámica.
¿Por qué queremos definir dinámicamente métodos? Tal vez para reducir la duplicación de código, o para añadir funcionalidad a otros objetos por ejemplo ActiveRecord (la herramienta ORM por defecto en Rails) lo utiliza en gran medida. Veamos un ejemplo:
class Book < ActiveRecord::Base
end
b = Book.new
b.title
Si estas familiarizado con ActiveRecord esta linea de código no le resultara extraña. Pero nunca definimos el atributo title, pero lo podemos utilizar, como? Bueno ActiveRecord por medio de metaprograming define un atributo de forma dinámica, dado que title es un atributo de la tabla.
Normalmente, llamando title en esta clase debería lanzar error NoMethodError - pero ActiveRecord añade dinámicamente métodos al igual que estamos a punto de hacer. ActiveRecord es un excelente ejemplo de cómo se puede utilizar metaprogramming al máximo.
Crear nuestros propios métodos:
def foo
puts "foo was called"
end
def baz
puts "baz was called"
end
def bar
puts "bar was called"
end
foo
baz
bar
# => foo was called
# => baz was called
# => bar was called
Si, si estamos duplicando codigo. Usemos metaprogramming:
%w(foo baz bar).each do |s|
define_method(s) do
puts "#{s} was called"
end
end
foo
baz
bar
# => foo was called
# => baz was called
# => bar was called
Llamar un método de forma dinámica:
%w(test1 test2 test3 test4 test5).each do |s|
define_method(s) do
puts "#{s} was called"
end
end
# New Code
(1..5).each { |n| send("test#{n}") }
# => test1 was called
# => test2 was called
# => test3 was called
# => test4 was called
# => test5 was called
Como se puede ver con send llamamos un método de forma dinámica. Y send es un método de Object.
class OKCRB
def is_boss?
puts "true"
end
end
okcrb = OKCRB.new
okcrb.send("is_boss?")
# => true
Ojo acá!! send nos permite llamar a métodos privados por lo que podemos romper el encapsulamiento. Si no queremos hacer esto, podemos llamar a public_send que no nos permite llamar métodos privados.
Que pasa si hacemos esto:
class Book
end
b = Book.new
b.read
Por supuesto obtendremos un error, un NoMethodError. Ahora bien podemos hacer lo siguiente:
class Book
def method_missing(method, *args, &block)
puts "You called: #{method}(#{args.join(', ')})"
puts "(You also passed it a block)" if block_given?
end
end
b = Book.new
b.read
b.read('a', 'b') { "foo" }
# => You called: read()
# => You called read(a, b)
# => (You also passed it a block)
BasicObject#method_missing nos permite controlar el error NoMethodError. De esta forma, el error nunca sucede y aquí podemos hacer lo que deseemos.
Este es un pequeño y humilde resumen de metaprograming en Ruby, es decir por hoy estamos!!
Dejo link:
https://thesocietea.org/2015/08/metaprogramming-in-ruby-part-1/