A menudo oyes que la metaprogramación es algo que sólo usan los ninjas de Ruby, y que simplemente no es para el común de los mortales. Pero la verdad es que la metaprogramación no es algo que dé miedo en absoluto. Esta entrada del blog servirá para desafiar este tipo de pensamiento y acercar la metaprogramación al desarrollador medio de Ruby para que también pueda aprovechar sus beneficios.
Hay que tener en cuenta que la metaprogramación podría significar mucho y que a menudo puede ser muy mal utilizada y llegar al extremo cuando se trata de su uso, por lo que intentaré lanzar algunos ejemplos del mundo real que todo el mundo podría utilizar en la programación diaria.
Metaprogramación
La metaprogramación es una técnica mediante la cual se puede escribir código que escribe código por sí mismo de forma dinámica en tiempo de ejecución. Esto significa que puedes definir métodos y clases durante el tiempo de ejecución. Una locura, ¿verdad? En pocas palabras, usando la metaprogramación puedes reabrir y modificar clases, atrapar métodos que no existen y crearlos sobre la marcha, crear código que sea DRY evitando repeticiones, y mucho más.
Los fundamentos
Antes de sumergirnos en la metaprogramación en serio debemos explorar los fundamentos. Y la mejor manera de hacerlo es con ejemplos. Empecemos con uno y entendamos la metaprogramación en Ruby paso a paso. Probablemente puedas adivinar lo que hace este código:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend
Hemos definido una clase con dos métodos. El primer método de esta clase es un método de clase y el segundo es un método de instancia. Esto es algo básico en Ruby, pero detrás de este código ocurren muchas más cosas que debemos entender antes de seguir adelante. Vale la pena señalar que la propia clase Developer
es en realidad un objeto. En Ruby todo es un objeto, incluidas las clases. Como Developer
es una instancia, es una instancia de la clase Class
. Here is how the Ruby object model looks like:
p Developer.class # Classp Class.superclass # Modulep Module.superclass # Objectp Object.superclass # BasicObject
One important thing to understand here is the meaning of self
. The frontend
method is a regular method that is available on instances of class Developer
, but why is backend
method a class method? Every piece of code executed in Ruby is executed against a particular self. When the Ruby interpreter executes any code it always keeps track of the value self
for any given line. self
is always referring to some object but that object can change based on the code executed. For example, inside a class definition, the self
refers to the class itself which is an instance of class Class
.
class Developer p self end# Developer
Inside instance methods, self
refers to an instance of the class.
class Developer def frontend self endend p Developer.new.frontend# #<Developer:0x2c8a148>
Dentro de los métodos de clase, self
se refiere a la propia clase de una manera (que se discutirá con más detalle más adelante en este artículo):
class Developer def self.backend self endendp Developer.backend# Developer
Esto está bien, pero ¿qué es un método de clase después de todo? Antes de responder a esa pregunta tenemos que mencionar la existencia de algo llamado metaclase, también conocido como clase singleton y eigenclass. ¡El método de clase frontend
que hemos definido antes no es más que un método de instancia definido en la metaclase para el objeto Developer
! Una metaclase es esencialmente una clase que Ruby crea e inserta en la jerarquía de herencia para albergar los métodos de la clase, y así no interferir con las instancias que se crean a partir de la clase.
Metaclases
Cada objeto en Ruby tiene su propia metaclase. De alguna manera es invisible para un desarrollador, pero está ahí y se puede utilizar muy fácilmente. Como nuestra clase Developer
es esencialmente un objeto, tiene su propia metaclase. Como ejemplo vamos a crear un objeto de una clase String
y manipular su metaclase:
example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT
Lo que hemos hecho aquí es añadir un método singleton something
a un objeto. La diferencia entre los métodos de clase y los métodos singleton es que los métodos de clase están disponibles para todas las instancias de un objeto de clase mientras que los métodos singleton sólo están disponibles para esa única instancia. Los métodos de clase son muy utilizados mientras que los métodos singleton no tanto, pero ambos tipos de métodos se añaden a una metaclase de ese objeto.
El ejemplo anterior podría reescribirse así:
example = "I'm a string object"class << example def something self.upcase endend
La sintaxis es diferente pero efectivamente hace lo mismo. Ahora volvamos al ejemplo anterior donde creamos la clase Developer
y exploremos algunas otras sintaxis para definir un método de clase:
class Developer def self.backend "I am backend developer" endend
Esta es una definición básica que casi todo el mundo utiliza.
def Developer.backend "I am backend developer"end
This is the same thing, we are defining the backend
class method for Developer
. We didn’t use self
but defining a method like this effectively makes it a class method.
class Developer class << self def backend "I am backend developer" end endend
Again, we are defining a class method, but using syntax similar to one we used to define a singleton method for a String
object. You may notice that we used self
here which refers to a Developer
object itself. First we opened Developer
class, making self equal to the Developer
class. Next, we do class << self
, making self equal to Developer
‘s metaclass. Then we define a method backend
on Developer
‘s metaclass.
class << Developer def backend "I am backend developer" endend
By defining a block like this, we are setting self
to Developer
‘s metaclass for the duration of the block. As a result, the backend
method is added to Developer
‘s metaclass, rather than the class itself.
Let’s see how this metaclass behaves in the inheritance tree:
As you saw in previous examples, there’s no real proof that metaclass even exists. But we can use a little hack that can show us the existence of this invisible class:
class Object def metaclass_example class << self self end endend
If we define an instance method in Object
class (yes, we can reopen any class anytime, that’s yet another beauty of metaprogramming), we will have a self
referring to the Object
object inside it. Podemos entonces utilizar la sintaxis class << self
para cambiar el self actual para que apunte a la metaclase del objeto actual. Como el objeto actual es la clase Object
esta sería la metaclase de la instancia. El método devuelve self
que es en este punto una metaclase en sí misma. Así que llamando a este método de instancia sobre cualquier objeto podemos obtener una metaclase de ese objeto. Definamos de nuevo nuestra clase Developer
y empecemos a explorar un poco:
Y para el crescendo, veamos la prueba de que frontend
es un método de instancia de una clase y backend
es un método de instancia de una metaclase:
Aunque, para conseguir la metaclase no necesitas realmente reabrir Object
y añadir este hack. Puedes usar singleton_class
que proporciona Ruby. Es lo mismo que metaclass_example
que añadimos pero con este hack puedes ver realmente cómo funciona Ruby bajo el capó:
p developer.class.singleton_class.instance_methods false#
Definiendo métodos usando «class_eval» e «instance_eval»
Hay una forma más de crear un método de clase, y es usando instance_eval
:
Este trozo de código el intérprete de Ruby lo evalúa en el contexto de una instancia, que en este caso es un objeto Developer
. Y cuando se define un método sobre un objeto se está creando un método de clase o un método singleton. En este caso es un método de clase -para ser exactos, los métodos de clase son métodos singleton pero de una clase, mientras que los otros son métodos singleton de un objeto.
Por otro lado, class_eval
evalúa el código en el contexto de una clase en lugar de una instancia. Prácticamente reabre la clase. Así es como class_eval
puede utilizarse para crear un método de instancia:
Para resumir, cuando se llama al método class_eval
, se cambia self
para referirse a la clase original y cuando se llama a instance_eval
self
cambia para referirse a la metaclase de la clase original.
Definiendo los métodos que faltan sobre la marcha
Una pieza más del puzzle de la metaprogramación es method_missing
. Cuando llamas a un método en un objeto, Ruby primero entra en la clase y busca sus métodos de instancia. Si no encuentra el método allí, continúa buscando en la cadena de ancestros. Si Ruby sigue sin encontrar el método, llama a otro método llamado method_missing
que es un método de instancia de Kernel
que todos los objetos heredan. Como estamos seguros de que Ruby va a llamar a este método eventualmente para los métodos que faltan, podemos usar esto para implementar algunos trucos.
define_method
es un método definido en la clase Module
que puedes usar para crear métodos dinámicamente. Para usar define_method
, lo llamas con el nombre del nuevo método y un bloque donde los parámetros del bloque se convierten en los parámetros del nuevo método. ¿Cuál es la diferencia entre usar def
para crear un método y define_method
? No hay mucha diferencia, excepto que puedes usar define_method
en combinación con method_missing
para escribir código DRY. Para ser exactos, puedes utilizar define_method
en lugar de def
para manipular los ámbitos al definir una clase, pero eso es otra historia. Veamos un ejemplo sencillo:
Esto muestra cómo se utilizó define_method
para crear un método de instancia sin utilizar un def
. Sin embargo, hay mucho más que podemos hacer con ellos. Echemos un vistazo a este fragmento de código:
class Developer def coding_frontend p "writing frontend" end def coding_backend p "writing backend" endenddeveloper = Developer.newdeveloper.coding_frontend# "writing frontend"developer.coding_backend# "writing backend"
Este código no es DRY, pero usando define_method
podemos hacerlo DRY:
Eso es mucho mejor, pero todavía no es perfecto. ¿Por qué? Si queremos añadir un nuevo método coding_debug
por ejemplo, tenemos que poner este "debug"
en el array. Pero usando method_missing
podemos solucionar esto:
Este trozo de código es un poco complicado así que vamos a desglosarlo. Llamar a un método que no existe disparará method_missing
. Aquí, queremos crear un nuevo método sólo cuando el nombre del método comienza con "coding_"
. De lo contrario, simplemente llamamos a super para que haga el trabajo de informar sobre un método que en realidad no existe. Y simplemente usamos define_method
para crear ese nuevo método. Y ya está. Con este trozo de código podemos crear literalmente miles de nuevos métodos empezando por "coding_"
, y ese hecho es el que hace que nuestro código sea DRY. Como define_method
resulta ser privado para Module
, tenemos que usar send
para invocarlo.
Resumiendo
Esto es sólo la punta del iceberg. Para convertirse en un Jedi de Ruby, este es el punto de partida. Después de que domines estos bloques de construcción de la metaprogramación y entiendas realmente su esencia, puedes proceder a algo más complejo, por ejemplo crear tu propio Lenguaje de Dominio Específico (DSL). El DSL es un tema en sí mismo, pero estos conceptos básicos son un prerrequisito para entender los temas avanzados. Algunas de las gemas más usadas en Rails se construyeron de esta manera y probablemente usaste su DSL sin siquiera saberlo, como RSpec y ActiveRecord.
Esperemos que este artículo pueda acercarte un poco más a la comprensión de la metaprogramación y tal vez incluso a la construcción de tu propio DSL, que puedes usar para codificar más eficientemente.