La metaprogramación en Ruby es aún más genial de lo que parece

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.

Metaprogramación en Ruby: Código Escribiendo Código

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_evalself 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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada.