Entendiendo la metaprogramación con Ruby

Entendiendo la metaprogramación con Ruby

¿Qué es metaprogramación?

Según la definición que encontramos en Wikipedia :

La meta-programación consiste en escribir programas que escriben o manipulan otros programas (o a sí mismos) como datos, o que hacen en tiempo de compilación parte del trabajo que, de otra forma, se haría en tiempo de ejecución. Esto permite al programador ahorrar tiempo en la producción de código.

Pero una definición mas concisa y a mi parecer más clara la encontré en este artículo:

Es la habilidad de usar código para generar código

Existen múltiples técnicas para lograr este objetivo, una de la más sencilla y en su tiempo bastante utilizada, consistía en generar instrucciones de código utilizando variables del tipo String y concatenandolas o escribiéndolas en archivos planos.

code = "<?php \n"
code += "  $x = 0; \n"
(1..3).each do |n|
  code += "  $x = $x + #{n}; \n" 
end
code += "  print($x); \n?>"

File.open("dummy.php", 'w') { |file| file.write(code) }

Generará un archivo con el siguiente código PHP:

<?php 
  $x = 0; 
  $x = $x + 1; 
  $x = $x + 2; 
  $x = $x + 3; 
  print($x); 
?>

Con la creación y evolución de nuevas técnicas para llevar a cabo esta tarea, nos encontramos con nuevos conceptos que hacen parte del universo de la metaprogramación como son la Introspección y la Reflexión.

Introspección

La definición general de la palabra según Wikipedia :

“Introspección o inspección interna designa la idea de «mirar al interior».”

En nuestro caso se convertiría en la habilidad que tiene un programa para analizar su entorno durante la ejecución, habilidad que le permite interrogar a los objetos que lo componen.

Reflexión

La definición general de la palabra según Wikipedia :

“Es el proceso que permite pensar detenidamente en algo con la finalidad de sacar conclusiones.”

Esto sería la habilidad de ver y alterar la información de los elementos del programa durante la ejecución, habilidad que le permitiría extender o modificar los objetos sobre la marcha.

¿Cómo aplicar metaprogramación con Ruby?

1_sZSVVtdP9TE3mUoGh4GoYA.png

“Ruby es un lenguaje de programación interpretado, reflexivo y orientado a objetos, creado por el programador japonés Yukihiro 'Matz' Matsumoto”

Si vemos la definición en Wikipedia del lenguaje de programación Ruby, podemos notar una característica muy particular, que es reflexivo, esto nos da una pista sobre que el lenguaje está construido con esta propiedad en mente y que es muy posible que nos proporcione herramientas para lograr este cometido de una manera más amigable.

En algún momento hemos escuchado la frase “dentro del lenguaje Ruby todo es un objeto”, pero pienso que en la arquitectura en la que fue inspirado y diseñado el lenguaje, lleva este concepto a un nivel diferente.

42.class          # => Fixnum
:foo.class        # => Symbol
"Ruby".class      # => String
true.class        # => TrueClass
[1, 2, 3].class   # => Array
{ a: => 1 }.class # => Hash

Así podemos ver como un aparentemente y simple número o cadena de caracteres, en realidad son objetos o instancias de Clases y de igual manera podemos ver aplicado el concepto de Introspección al utilizar el método .class en un objeto para saber a qué Clase pertenece.

Incluso una clase es un objeto en sí; su clase pertenece a una clase especial llamada Class y todas las clases creadas en Ruby en realidad son una instancia de la clase Class:

Fixnum.class   # => Class
42.class.class # => Class

Class.class    # => Class

1_UtqAPW9-Cx7aYwWGv-ltyw.jpeg

Class implementa algunos interesantes métodos de introspección como:

class                          # Retorna el objeto de clase a la que pertenece el objeto instanciado
superclass                     # Retorna el objeto de clase del cual hereda la clase del objeto instanciado
ancestors                      # Retorna un Array con el listado de la cadena de ancestros 

instance_variables             # Retorna un Array con el listado de las variables de instancia creadas en la clase del objeto
instance_variable_get(name)    # Retorna el valor de una variable de instancia creada en la clase del objeto
class_variables                # Retorna un Array con el listado de las variables de clase creadas en la clase del objeto

methods                        # Retorna un Array con el listado de los metodos implementados dentro de la clase del objeto
public_methods                 # Retorna un Array con el listado de los metodos publicos implementados dentro de la clase del objeto
...

Y de reflexión como:

instance_variable_set(name, value)    # Nos permite definir y asignar un valor a una nueva variable de instancia

define_method(name, &block)           # Nos permite definir dentro de la clase un método de manera dinámica y en tiempo de ejecución

method_missing(name, *args, &block)   # Nos permite capturar y manejar la excepción que se dispara cuando llamamos un método no definido dentro de una clase.
...

Gracias a métodos como estos dentro de la clase Class, podemos manipular nuestras propias clases de manera Reflexiva:

class Example
end

ex = Example.new
ex.instance_variables                # => []
ex.instance_variable_set(:@x, 1)     # => 1
ex.instance_variables                # => ["@x"]
ex.instance_variable_defined?(:@x)   # => true
ex.instance_variable_defined?(:@y)   # => false
ex.instance_variable_get(:@x)        # => 1
ex.x                                 # => undefined method `x' for #<Example:0x000056174cbb0440 @x=1>

Conociendo la existencia de estos métodos, podemos entender de una mejor manera lo que se denominan “Macros a nivel de clases” o simplemente métodos a nivel de clase que generan código detrás de escena:

# Esta definición:
class Example
  attr_accessor :x
end

## Es equivalente a:
class Example
  def x 
    @x
  end

  def x=(value)
    @x = value
  end
end

ex.x  # => 1
Class.methods.select { |m| m =~ /attr/ }  # [:attr, :attr_reader, :attr_writer, :attr_accessor]

Como podemos ver attr_accessor no pertenece o es una palabra clave (keyword) dentro del lenguaje Ruby, sino que representa un método de la clase Class que genera código Ruby dentro de otras clases de manera dinámica.

Veamos con un ejemplo cómo podemos definir nuestras propias macros a nivel de clases, en este caso realizaremos nuestra propia implementación del método attr_accessor el cual realizara la creación de las variables de instancia, de los métodos setter y getter y adicional imprimirá por consola un mensaje personalizado:

# Clase base con la definición de nuestra macro a nivel de clase:
class AttrCustom
  def self.attr_custom(name)
    define_method("#{name}=") do |value|
      puts "Asignando #{value.inspect} a #{name}"
      instance_variable_set("@#{name}", value)
    end

    define_method("#{name}") do
      puts "Leyendo #{name}"
      instance_variable_get("@#{name}")
    end
  end
end

# Clase con implementación por herencia de nuestra macro:
class AnotherExample < AttrCustom
  attr_custom :z
end

ex = AnotherExample.new
ex.z = 5     # => Asignando 5 a z
ex.z         # => Leyendo z

Para este ejemplo hicimos uso del método define_method de la clase Class el cual nos permite realizar la definición de un método dentro de la clase de manera dinámica y en tiempo de ejecución.

Otro de los métodos más populares utilizados para cambiar el flujo de un programa en tiempo de ejecución sería method_missing es un método gancho (“ hook method ” o “callback”) llamado por Ruby cuando algún método no es encontrado durante el proceso de “búsqueda de método” ( method lookup ):

class Student
  def method_missing(name)
    puts "No fue encontrado el método #{name}"
  end
end

student = Student.new
student.reflection
# No fue encontrado el método reflection
class MethodCatcher
  def method_missing(name, *args, &block)
    puts "El nombre del método no encontrado es #{name}"
    puts "los argumentos del método son #{args}"
    puts "El cuerpo del método es #{block.inspect}"
  end
end

catch = MethodCatcher.new
catch.some_method(1, 2) { puts "something" }
# El nombre del método no encontrado es some_method
# los argumentos del método son [1, 2]
# El cuerpo del método es #<Proc:0x0033...>

Nos puede surgir la pregunta de cómo este “manejo particular de una excepción” nos puede ayudar a construir programas dinámicos que cambian su ruta de ejecución, bueno para esto si debemos usar bastante nuestra creatividad e imaginación para poder obtener resultados como estos:

class HTML
  def self.method_missing(method_name, *args, &block)
    tag(method_name, *args, &block)
  end

  def self.tag(tag_name, *args, &block)
    "<#{tag_name}>#{args.last} #{yield if block_given?}</#{tag_name}>"
  end
end

html = HTML.div do
  HTML.header do
     HTML.h1 "Titulo de la página"
  end +
  HTML.article do
    HTML.p "Hola a todos"
  end
end

# <div>
#   <header>
#     <h1>Titulo de la página</h1>
#   </header>
#   <article>
#     <p>Hola a todos</p>
#   </article>
# </div>

fuente: https://codigofacilito.com/articulos/que-es-metaprogramacion

En este ejemplo vemos como con el uso del método method_missing y la habilidad de pasar bloques de código (blocks, procs o lambdas) como parámetros, en pocas lineas de código podemos construir un hermoso DSL para manipular y generar código HTML.

Conclusión

Vimos el concepto de metaprogramación como técnica para el desarrollo de software, detallamos conceptos asociados a esta como lo son la Introspección y Reflexión y revisamos como un lenguaje de programación como Ruby nos entrega diversas herramientas para utilizar esta técnica de una manera amigable.

Referencias:

https://es.wikipedia.org/wiki/Metaprogramaci%C3%B3n https://codigofacilito.com/articulos/que-es-metaprogramacion https://es.wikipedia.org/wiki/Introspecci%C3%B3n https://hackademy.io/tutoriel-videos/introduction-ruby-reflexion https://es.slideshare.net/kim.mens/reflection-in-ruby http://ruby-metaprogramming.rubylearning.com/html/ruby_metaprogramming_2.html http://rubymonk.com/learning/books/2-metaprogramming-ruby/chapters/25-dynamic-methods/lessons/72-define-method