A Metaprogramação em Ruby é ainda mais fixe do que soa

Ouvimos frequentemente que a metaprogramação é algo que só os ninjas Ruby usam, e que simplesmente não é para os mortais comuns. Mas a verdade é que a meta-programação não é algo assustador de todo. Este post no blog vai servir para desafiar este tipo de pensamento e para aproximar a metaprogramação do desenvolvedor Ruby médio para que eles também possam colher seus benefícios.

Ruby Metaprogramming: Código de escrita de código

De notar que a metaprogramação pode significar muito e pode muitas vezes ser muito mal utilizada e ir ao extremo quando se trata de utilização, por isso vou tentar dar alguns exemplos do mundo real que todos poderiam usar na programação diária.

Metaprogramming

Metaprogramming é uma técnica pela qual se pode escrever código por si só dinamicamente em tempo de execução. Isto significa que você pode definir métodos e classes durante o tempo de execução. Louco, certo? Em resumo, usando a metaprogramação você pode reabrir e modificar classes, pegar métodos que não existem e criá-los na mosca, criar código que é DRY evitando repetições, e mais.

The Basics

Antes de mergulharmos em metaprogramação séria devemos explorar o básico. E a melhor maneira de fazer isso é através do exemplo. Vamos começar com um e entender a metaprogramação de Ruby passo a passo. Você pode provavelmente adivinhar o que este código está fazendo:

class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" endend

Definimos uma classe com dois métodos. O primeiro método nesta classe é um método de classe e o segundo é um método de instância. Isto é material básico em Ruby, mas há muito mais a acontecer por detrás deste código que precisamos de compreender antes de prosseguirmos. Vale a pena salientar que a classe Developer em si mesma é realmente um objecto. Em Ruby tudo é um objecto, incluindo as classes. Uma vez que Developer é uma instância, é uma instância da classe 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 dos métodos de classe, self refere-se à classe em si de uma forma (que será discutida mais detalhadamente mais adiante neste artigo):

class Developer def self.backend self endendp Developer.backend# Developer

Isso é bom, mas afinal o que é um método de classe? Antes de responder a essa pergunta precisamos mencionar a existência de algo chamado metaclasse, também conhecido como classe singleton e eigenclass. Class method frontend que definimos anteriormente não passa de um método de instância definido na metaclasse para o objeto Developer! Uma metaclasse é essencialmente uma classe que o Ruby cria e insere na hierarquia da herança para guardar métodos de classe, não interferindo assim com instâncias que são criadas a partir da classe.

Metaclasses

Todos os objectos em Ruby têm a sua própria metaclasse. Ele é de alguma forma invisível para um desenvolvedor, mas está lá e você pode usá-lo muito facilmente. Uma vez que a nossa classe Developer é essencialmente um objecto, ele tem a sua própria metaclasse. Como exemplo vamos criar um objecto de uma classe String e manipular a sua metaclasse:

example = "I'm a string object"def example.something self.upcaseendp example.something# I'M A STRING OBJECT

O que fizemos aqui foi adicionar um método de um único botão something a um objecto. A diferença entre métodos de classe e métodos singleton é que métodos de classe estão disponíveis para todas as instâncias de um objeto de classe enquanto métodos singleton estão disponíveis apenas para aquela instância única. Métodos de classe são amplamente utilizados enquanto métodos singleton não tanto, mas ambos os tipos de métodos são adicionados a uma metaclasse daquele objeto.

O exemplo anterior poderia ser reescrito assim:

example = "I'm a string object"class << example def something self.upcase endend

A sintaxe é diferente mas efetivamente faz a mesma coisa. Agora vamos voltar ao exemplo anterior onde criámos Developer class e explorar algumas outras sintaxes para definir um método de classe:

class Developer def self.backend "I am backend developer" endend

Esta é uma definição básica que quase toda a gente usa.

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 então usar class << self sintaxe para mudar o eu atual para apontar para a metaclasse do objeto atual. Como o objeto corrente é Object classe em si, esta seria a metaclasse da instância. O método retorna self que é, neste ponto, uma metaclasse em si. Então chamando este método de instância em qualquer objeto, podemos obter uma metaclasse desse objeto. Vamos definir nosso Developer classe novamente e começar a explorar um pouco:

E para o crescendo, vamos ver a prova de que frontend é um método de instância de uma classe e backend é um método de instância de uma metaclasse:

embora, para obter a metaclasse você não precisa reabrir Object e adicionar este hack. Pode usar singleton_class que o Ruby fornece. É o mesmo que metaclass_example que nós adicionamos mas com este hack pode realmente ver como o Ruby funciona por baixo do hood:

p developer.class.singleton_class.instance_methods false# 

Definindo Métodos Usando “class_eval” e “instance_eval”

Existe mais uma forma de criar um método de classe, e que é usando instance_eval:

Este código Ruby interpreter avalia no contexto de uma instância, que neste caso é um objecto Developer. E quando você está definindo um método em um objeto você está criando ou um método de classe ou um método de um único botão. Neste caso, é um método de classe – para ser exato, métodos de classe são métodos singleton mas métodos singleton de uma classe, enquanto os outros são métodos singleton de um objeto.

Por outro lado, class_eval avalia o código no contexto de uma classe ao invés de uma instância. Ele praticamente reabre a classe. Aqui está como class_eval pode ser usado para criar um método de instância:

Para resumir, quando você chama class_eval método, você muda self para se referir à classe original e quando você chama instance_evalself muda para se referir à metaclasse da classe original.

Definindo métodos ausentes na mosca

Mais uma peça de metaprogramação é method_missing. Quando você chama um método em um objeto, Ruby primeiro vai para a classe e navega pelos seus métodos de instância. Se não encontrar o método lá, continua a procurar na cadeia dos antepassados. Se o Ruby ainda não encontrar o método, ele chama outro método chamado method_missing que é um método de instância de Kernel que todo o objecto herda. Como temos a certeza que o Ruby vai eventualmente chamar a este método por métodos em falta, podemos usar isto para implementar alguns truques.

define_method é um método definido em Module classe que pode usar para criar métodos dinamicamente. Para usar define_method, chama-se o método com o nome do novo método e um bloco onde os parâmetros do bloco se tornam os parâmetros do novo método. Qual é a diferença entre usar def para criar um método e define_method? Não há muita diferença exceto que você pode usar define_method em combinação com method_missing para escrever código DRY. Para ser exato, você pode usar define_method em vez de def para manipular escopos ao definir uma classe, mas isso é toda uma outra história. Vejamos um exemplo simples:

Isto mostra como define_method foi usado para criar um método de instância sem usar um def. No entanto, há muito mais que podemos fazer com eles. Vamos dar uma olhada neste trecho 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 não é DRY, mas usando define_method podemos torná-lo DRY:

Isso é muito melhor, mas ainda não perfeito. Porquê? Se queremos adicionar um novo método coding_debug por exemplo, precisamos de colocar isto "debug" no array. Mas usando method_missing podemos corrigir isto:

Este pedaço de código é um pouco complicado, então vamos quebrá-lo. Chamando um método que não existe irá disparar method_missing. Aqui, queremos criar um novo método apenas quando o nome do método começa com "coding_". Caso contrário, apenas chamamos super para fazer o trabalho de reportar um método que está realmente faltando. E nós estamos simplesmente usando define_method para criar esse novo método. É isso mesmo! Com este pedaço de código podemos criar literalmente milhares de novos métodos começando com "coding_", e esse fato é o que torna o nosso código SECO. Como define_method é privado para Module, precisamos de usar send para o invocar.

Brato para cima

Esta é apenas a ponta do iceberg. Para se tornar um Jedi Ruby, este é o ponto de partida. Depois de dominar estes blocos de construção de meta-programação e compreender verdadeiramente a sua essência, pode prosseguir para algo mais complexo, por exemplo, criar a sua própria Linguagem específica de Domínio (DSL). A DSL é um tópico em si, mas estes conceitos básicos são um pré-requisito para compreender tópicos avançados. Algumas das gemas mais usadas no Rails foram construídas dessa forma e você provavelmente usou sua DSL sem mesmo conhecê-la, como RSpec e ActiveRecord.

esperançosamente este artigo pode lhe dar um passo para entender a metaprogramação e talvez até mesmo construir sua própria DSL, que você pode usar para codificar mais eficientemente.

Deixe uma resposta

O seu endereço de email não será publicado.