A metaprogramação Ruby é ainda mais legal do que parece
Publicados: 2022-03-11Você costuma ouvir que metaprogramação é algo que apenas ninjas Ruby usam, e que simplesmente não é para mortais comuns. Mas a verdade é que a metaprogramação não é nada assustador. Esta postagem no blog servirá para desafiar esse tipo de pensamento e aproximar a metaprogramação do desenvolvedor Ruby médio para que eles também possam colher seus benefícios.
Deve-se notar que a metaprogramação pode significar muito e muitas vezes pode ser muito mal utilizada e ir ao extremo quando se trata de uso, então tentarei lançar alguns exemplos do mundo real que todos poderiam usar na programação cotidiana.
Metaprogramação
Metaprogramação é uma técnica pela qual você pode escrever código que escreve código por si só dinamicamente em tempo de execução. Isso significa que você pode definir métodos e classes durante o tempo de execução. Louco, certo? Em poucas palavras, usando metaprogramação você pode reabrir e modificar classes, capturar métodos que não existem e criá-los em tempo real, criar código DRY evitando repetições e muito mais.
O básico
Antes de mergulharmos em metaprogramação séria, devemos explorar o básico. E a melhor maneira de fazer isso é pelo exemplo. Vamos começar com um e entender a metaprogramação Ruby passo a passo. Você provavelmente pode adivinhar o que este código está fazendo:
class Developer def self.backend "I am backend developer" end def frontend "I am frontend developer" end end
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. Isso é básico em Ruby, mas há muito mais acontecendo por trás desse código que precisamos entender antes de prosseguirmos. Vale ressaltar que a própria classe Developer
é na verdade um objeto. Em Ruby tudo é um objeto, incluindo classes. Como Developer
é uma instância, é uma instância da classe Class
. Aqui está como o modelo de objeto Ruby se parece:
p Developer.class # Class p Class.superclass # Module p Module.superclass # Object p Object.superclass # BasicObject
Uma coisa importante a entender aqui é o significado de self
. O método frontend
é um método regular que está disponível em instâncias da classe Developer
, mas por que o método backend
é um método de classe? Cada pedaço de código executado em Ruby é executado em um self específico. Quando o interpretador Ruby executa qualquer código, ele sempre acompanha o valor self
de qualquer linha. self
está sempre se referindo a algum objeto, mas esse objeto pode mudar com base no código executado. Por exemplo, dentro de uma definição de classe, o self
se refere à própria classe que é uma instância da classe Class
.
class Developer p self end # Developer
Dentro dos métodos de instância, self
se refere a uma instância da classe.
class Developer def frontend self end end p Developer.new.frontend # #<Developer:0x2c8a148>
Dentro dos métodos de classe, self
se refere à própria classe de uma maneira (que será discutida com mais detalhes posteriormente neste artigo):
class Developer def self.backend self end end p Developer.backend # Developer
Isso é bom, mas o que é um método de classe afinal? Antes de responder a essa pergunta, precisamos mencionar a existência de algo chamado metaclasse, também conhecido como classe singleton e autoclasse. O frontend
de método de classe que definimos anteriormente nada mais é do que um método de instância definido na metaclasse para o objeto Developer
! Uma metaclasse é essencialmente uma classe que Ruby cria e insere na hierarquia de herança para conter métodos de classe, não interferindo com instâncias que são criadas a partir da classe.
Metaclasses
Cada objeto em Ruby tem sua própria metaclasse. É de alguma forma invisível para um desenvolvedor, mas está lá e você pode usá-lo com muita facilidade. Como nossa classe Developer
é essencialmente um objeto, ela tem sua própria metaclasse. Como exemplo vamos criar um objeto da classe String
e manipular sua metaclasse:
example = "I'm a string object" def example.something self.upcase end p example.something # I'M A STRING OBJECT
O que fizemos aqui foi adicionar um método singleton a something
objeto. A diferença entre métodos de classe e métodos singleton é que os métodos de classe estão disponíveis para todas as instâncias de um objeto de classe, enquanto os métodos singleton estão disponíveis apenas para essa única instância. Os métodos de classe são amplamente usados, enquanto os métodos singleton nem tanto, mas ambos os tipos de métodos são adicionados a uma metaclasse desse objeto.
O exemplo anterior poderia ser reescrito assim:
example = "I'm a string object" class << example def something self.upcase end end
A sintaxe é diferente, mas efetivamente faz a mesma coisa. Agora vamos voltar ao exemplo anterior onde criamos a classe Developer
e explorar algumas outras sintaxes para definir um método de classe:
class Developer def self.backend "I am backend developer" end end
Esta é uma definição básica que quase todo mundo usa.
def Developer.backend "I am backend developer" end
É a mesma coisa, estamos definindo o método de classe de backend
para Developer
. Nós não usamos self
, mas definir um método como esse efetivamente o torna um método de classe.
class Developer class << self def backend "I am backend developer" end end end
Novamente, estamos definindo um método de classe, mas usando uma sintaxe semelhante à que usamos para definir um método singleton para um objeto String
. Você pode notar que usamos self
aqui, que se refere ao próprio objeto Developer
. Primeiro abrimos a classe Developer
, tornando self igual à classe Developer
. Em seguida, fazemos a class << self
, tornando self igual à metaclasse do Developer
. Em seguida, definimos um backend
de método na metaclasse do Developer
.
class << Developer def backend "I am backend developer" end end
Ao definir um bloco como este, estamos definindo self
para a metaclasse do Developer
pela duração do bloco. Como resultado, o método backend
é adicionado à metaclasse do Developer
, em vez da própria classe.
Vamos ver como essa metaclasse se comporta na árvore de herança:
Como você viu nos exemplos anteriores, não há nenhuma prova real de que a metaclasse exista. Mas podemos usar um pequeno hack que pode nos mostrar a existência dessa classe invisível:
class Object def metaclass_example class << self self end end end
Se definirmos um método de instância na classe Object
(sim, podemos reabrir qualquer classe a qualquer momento, essa é outra beleza da metaprogramação), teremos um self
referindo-se ao objeto Object
dentro dela. Podemos então usar a sintaxe class << self
para alterar o self atual para apontar para a metaclasse do objeto atual. Como o objeto atual é a própria classe Object
, essa seria a metaclasse da instância. O método retorna self
que neste momento é uma metaclasse. Então, chamando esse método de instância em qualquer objeto, podemos obter uma metaclasse desse objeto. Vamos definir nossa classe Developer
novamente e começar a explorar um pouco:

class Developer def frontend p "inside instance method, self is: " + self.to_s end class << self def backend p "inside class method, self is: " + self.to_s end end end developer = Developer.new developer.frontend # "inside instance method, self is: #<Developer:0x2ced3b8>" Developer.backend # "inside class method, self is: Developer" p "inside metaclass, self is: " + developer.metaclass_example.to_s # "inside metaclass, self is: #<Class:#<Developer:0x2ced3b8>>"
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:
p developer.class.instance_methods false # [:frontend] p developer.class.metaclass_example.instance_methods false # [:backend]
Embora, para obter a metaclasse, você não precise reabrir Object
e adicionar este hack. Você pode usar singleton_class
que Ruby fornece. É o mesmo que metaclass_example
que adicionamos, mas com este hack você pode realmente ver como o Ruby funciona nos bastidores:
p developer.class.singleton_class.instance_methods false # [:backend]
Definindo métodos usando “class_eval” e “instance_eval”
Há mais uma maneira de criar um método de classe, e isso é usando instance_eval
:
class Developer end Developer.instance_eval do p "instance_eval - self is: " + self.to_s def backend p "inside a method self is: " + self.to_s end end # "instance_eval - self is: Developer" Developer.backend # "inside a method self is: Developer"
Este trecho de código interpretador Ruby avalia no contexto de uma instância, que neste caso é um objeto Developer
. E quando você está definindo um método em um objeto, você está criando um método de classe ou um método singleton. 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 em vez de uma instância. Praticamente reabre a aula. Aqui está como class_eval
pode ser usado para criar um método de instância:
Developer.class_eval do p "class_eval - self is: " + self.to_s def frontend p "inside a method self is: " + self.to_s end end # "class_eval - self is: Developer" p developer = Developer.new # #<Developer:0x2c5d640> developer.frontend # "inside a method self is: #<Developer:0x2c5d640>"
Para resumir, quando você chama o método class_eval
, você muda self
para se referir à classe original e quando você chama instance_eval
, self
muda para se referir à metaclass da classe original.
Definindo métodos ausentes em tempo real
Mais uma peça do quebra-cabeça de metaprogramação é method_missing
. Quando você chama um método em um objeto, o Ruby primeiro entra na classe e procura seus métodos de instância. Se não encontrar o método lá, ele continua a procurar na cadeia de ancestrais. 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 do Kernel
que todo objeto herda. Já que temos certeza de que Ruby vai chamar esse método eventualmente para métodos ausentes, podemos usar isso para implementar alguns truques.
define_method
é um método definido na classe Module
que você pode usar para criar métodos dinamicamente. Para usar define_method
, você o chama 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 é outra história. Vejamos um exemplo simples:
class Developer define_method :frontend do |*my_arg| my_arg.inject(1, :*) end class << self def create_backend singleton_class.send(:define_method, "backend") do "Born from the ashes!" end end end end developer = Developer.new p developer.frontend(2, 5, 10) # => 100 p Developer.backend # undefined method 'backend' for Developer:Class (NoMethodError) Developer.create_backend p Developer.backend # "Born from the ashes!"
Isso 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" end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
Este código não é DRY, mas usando define_method
podemos torná-lo DRY:
class Developer ["frontend", "backend"].each do |method| define_method "coding_#{method}" do p "writing " + method.to_s end end end developer = Developer.new developer.coding_frontend # "writing frontend" developer.coding_backend # "writing backend"
Isso é muito melhor, mas ainda não é perfeito. Por quê? Se quisermos adicionar um novo método coding_debug
, por exemplo, precisamos colocar esse "debug"
no array. Mas usando method_missing
podemos corrigir isso:
class Developer def method_missing method, *args, &block return super method, *args, &block unless method.to_s =~ /^coding_\w+/ self.class.send(:define_method, method) do p "writing " + method.to_s.gsub(/^coding_/, '').to_s end self.send method, *args, &block end end developer = Developer.new developer.coding_frontend developer.coding_backend developer.coding_debug
Este pedaço de código é um pouco complicado, então vamos dividi-lo. Chamar um método que não existe method_missing
. Aqui, queremos criar um novo método somente quando o nome do método começar com "coding_"
. Caso contrário, apenas chamamos super para fazer o trabalho de relatar um método que está realmente ausente. E estamos simplesmente usando define_method
para criar esse novo método. É isso! 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 nosso código DRY. Como define_method
é privado para Module
, precisamos usar send
para invocá-lo.
Empacotando
Esta é apenas a ponta do iceberg. Para se tornar um Ruby Jedi, este é o ponto de partida. Depois de dominar esses blocos de construção da metaprogramação e realmente entender sua essência, você pode prosseguir para algo mais complexo, por exemplo, criar sua própria Linguagem Específica de Domínio (DSL). DSL é um tópico em si, mas esses conceitos básicos são um pré-requisito para a compreensão de tópicos avançados. Algumas das gems mais usadas no Rails foram construídas dessa forma e você provavelmente usou o DSL dele mesmo sem saber, como RSpec e ActiveRecord.
Espero que este artigo possa levá-lo um passo mais perto de entender a metaprogramação e talvez até construir sua própria DSL, que você pode usar para codificar com mais eficiência.