Nome: Metaprogramação em Ruby
Autor: Bernardo Monteiro Rufino da Equipe Webly
Licença Creative Commons: Este conteúdo só pode ser copiado caso seja LINKADO para este original e citado o nome do autor antes de qualquer outro texto.



Primeiro vamos definir do que estamos falando, não vamos escrever código que gera outro código. A metaprogramação à qual me refiro é aquela onde criamos construções que definirão elementos no nosso código. Uma maneira de "criar" uma linguagem para resolver problemas (DSL) e não se repetir (DRY).

Por que Ruby? Ruby possui a maioria dos aspectos necessários à metaprogramação: é dinâmica, os elementos estão abertos a mudanças (Mixins), tem blocos de código (Closures, Code Blocks) que permitem definir novas estruturas de controle e também tem sintaxe simples, enxuta, expressiva e legível.

Ok, já falamos demais, vamos à prática, é preciso conhecimento em Ruby para entender os códigos.

Primeiro vamos ver um exemplo nativo da linguagem, attr_accessor, attr_reader e attr_writer, que são atalhos para getters e setters.

CODE
class Person
   attr_reader :name;
   attr_accessor :hair;
  
   def initialize(name, hair)
     @name, @hair = name, hair;
   end

end

jose = Person.new "Jose", :brown;
jose.name           #=> "Jose"
jose.hair           #=> :brown
jose.hair = :blond  #Pintou o cabelo
jose.hair           #=> :blond

Depois da definição da classe, na segunda linha, estamos chamando métodos da classe Module, não é sintaxe própria da linguagem e sim métodos. Nesse mesmo post reescreveremos esses métodos. Agora vou mostrar algumas técnicas que serão usadas para se fazer isso.


Usando Meta classes

Em Ruby tudo são objetos, e classes são um tipo especial de objeto a diferença é que podem conter métodos, métodos não são armazenados em objetos mas sim em suas respectivas classes, objetos armazenam variáveis de instância. Ainda assim podemos criar métodos particulares a um objeto que não afetam as outras instâncias.

CODE
class Dog
  
   def self.bark
     "UAU! from #{self.class}";
   end

   def bark
     "UAU! from #{self.class}";
   end
  
end

#Chamando os métodos da classe e dos objetos
Dog.bark         #=> "UAU! from Class"
first_dog = Dog.new
first_dog.bark   #=> "UAU! from Dog"
second_dog = Dog.new
second_dog.bark  #=> "UAU! from Dog"

#(1)Definindo um método particular a um objeto
def first_dog.walk
   "Walking..."
end

#O novo método só afeta aquele objeto
first_dog.walk   #=> "Walking..."
#second_dog.walk  #=> NoMethodError

#Acessando esses métodos
first_dog.singleton_methods   #=> ["walk"]
second_dog.singleton_methods  #=> []

Ué mas para onde estes métodos vão se os objetos não armazenam eles. Esses métodos são armazenados em uma Meta Classe (Metaclass ou Singleton Class) referente a esse objeto e pode ser acessada desse jeito:

CODE
metaclass = (class << first_dog; self; end);

Desse jeito poderíamos escrever o método walk, que foi escrito somente para first_dog, em second_dog usando sua meta classe.

CODE
class << second_dog
   def walk
     "Walking..."
   end
end

second_dog.walk               #=> "Walking..."
second_dog.singleton_methods  #=> ["walk"]

Agora parece que já entendemos tudo sobre meta classes. Mas ainda tem um detalhe, como classes são instâncias de Class os métodos de classe (ou estáticos), como o método bark de Dog definido em def self.bark, são armazenados em uma meta classe dessa classe.

CODE
#Métodos de classe
Dog.singleton_methods  #=> [..., "bark"]

#Diversas maneiras de se definir métodos de classe, lembre que self se refere a classe
class Dog
  
   #Usando self
   def self.foo1; "Foo"; end
  
   #Usando o nome Dog
   def Dog.foo2; "Foo"; end
  
   #Usando a meta classe da classe com self
   class << self
     def foo3; "Foo"; end
   end
  
   #Usando a meta classe da classe com o nome Dog
   class << Dog
     def foo4; "Foo"; end
   end

end

Dog.foo1               #=> "Foo"
Dog.foo2               #=> "Foo"
Dog.foo3               #=> "Foo"
Dog.foo4               #=> "Foo"
Dog.singleton_methods  #=> [..., "bark", "foo1", "foo2", "foo3", "foo4"]

Acredito que com o exemplo acima tudo se esclareceu. Agora vamos tornar o acesso às meta classes mais fácil.

CODE
class Object
  
   def metaclass
     class << self
       self
     end
   end

end

metaclass1 = (class << first_dog; self; end)  #O jeito antigo
metaclass2 = first_dog.metaclass              #Usando o método definido acima
metaclass1 == metaclass2                      #=> true

Pronto, agora você já sabe pra onde vão os métodos, o que são meta classes e para que servem.


Definindo métodos

Definir métodos dinamicamente é fácil, Ruby nos trás o método define_method para isso. Veja este exemplo abaixo:

CODE
class Pig
   define_method :count do
     @count = (@count || 0) + 1;
   end
end

pig = Pig.new
pig.count

Esse exemplo não mostrou muita coisa nova, já que poderíamos ter usado a palavra chave def. Mas esse método pode nos oferecer muitas vantagens, primeiro ele pode ser usado para criar métodos dinamicamente já que aceita um Symbol ou String como parâmetro para nome do método e segundo que aceita bloco de código para seu conteúdo. Poderíamos fazer isso avaliando uma String, mas isso é perigoso e muito menos "elegante".

Para chamá-lo fora da definição da classe (ou do modulo), temos que usar o método send e como parâmetro :define_method, ou então usar class_eval (ou module_eval que é igual), mas veremos isso depois. A visibilidade do método que ele irá criar será de acordo com a visibilidade atual, em top-level a visibilidade é private, então ele criará métodos privados até que declaremos public, por isso que se você usá-lo dentro de classes onde a visibilidade é publica por padrão os métodos definidos serão públicos.

CODE
Counter = Class.new
shared_count = 0 #Uma variável local que será compartilhada pelo método

public #Se não declarassemos o método abaixo seria privado
Counter.send(:define_method, :count) do
   shared_count += 1;
   @instance_count = (@instance_count || 0) + 1;
   {:shared => shared_count, :instance => @instance_count}
end
private #Voltamos à visibilidade padrão

first_counter = Counter.new
second_counter = Counter.new

first_counter.count   #=> {:shared=>1, :instance=>1}
second_counter.count  #=> {:shared=>2, :instance=>1}
first_counter.count   #=> {:shared=>3, :instance=>2}
second_counter.count  #=> {:shared=>4, :instance=>2}

Vimos nesse exemplo que podemos compartilhar o contexto através do bloco de código e que o método criado terá visibilidade igual à atual. Agora iremos declarar métodos só para um objeto usando as meta classes que já estudamos, note que nesse caso irei utilizar class_eval, assim além de não precisar usar send também não precisarei declarar public já que dentro já é publico.

CODE
#Usando class_eval, não precisamos declarar public
first_counter.metaclass.class_eval do
   define_method(:instance_count){@instance_count}
end

first_counter.instance_count   #=> 2
#second_counter.instance_count  #=> NoMethodError

Também podemos fazer em módulos, por que não? Abaixo mostro como podemos usar define_method dentro de module_eval também. Usamos extend para adicionar métodos de instância a um objeto quando usado com esse objeto ou para adicionar métodos de classe a uma classe quando usada com essa classe. Usamos include para adicionar métodos de instância de alguma classe quando usada com essa classe.

CODE
class Man
   def walk
     "Walking..."
   end
   include Runner
end

Runner.module_eval do
   define_method(:run){"Running..."}
end


woman = Object.new
woman.extend Runner
man = Man.new

woman.run #=> "Running..."
man.walk  #=> "Walking..."
man.run   #=> "Running..."


Acessando e modificando variáveis de instância

O truque que Rails usa para deixar as variáveis de instância do controller acessíveis na view é usar os métodos que Ruby nos oferece para manipular variáveis de instância, são eles instance_variables, instance_variable_get e instance_variable_set. Isso serve para copiar variáveis de um objeto à outro, o exemplo é simples de entender.

CODE
class FirstClass
   attr_reader :first, :second;
  
   def initialize
     @first = "First First";
     @second = "Second First";
     @first_uniq = "FirstClass"
   end
  
end

class SecondClass
   attr_reader :first, :second;
  
   def initialize
     @first = "First Second";
     @second = "Second Second";
     @second_uniq = "SecondClass"
   end
  
end

first = FirstClass.new
second = SecondClass.new

first.instance_variable_get("@first")      #=> "First First"
first.instance_variable_get(:@second)      #=> "Second First"

second.instance_variables                  #=> ["@second_uniq", "@first", "@second"]
second.first                               #=> "First Second"
second.instance_variable_set(:@first, "First Outside")
second.first                               #=> "First Outside"
second.instance_variable_set(:@first, "Second Outside")

#Copying the instance variables from 'second' to 'first'
for var in second.instance_variables
   first.instance_variable_set(var, second.instance_variable_get(var))
end

#Iterating the array with the variables and printing them
for var in first.instance_variables.sort
   puts "#{var} = #{first.instance_variable_get(var).inspect}"
end

#@first = "Second Outside"
#@first_uniq = "FirstClass"
#@second = "Second Second"
#@second_uniq = "SecondClass"


Diferentes versões de eval

Existem diversas versões de comandos eval em Ruby. São elas eval, instance_eval, class_eval e module_eval, lembrando que module_eval é o mesmo que class_eval. A primeira, eval só aceita String como argumento para ser executado, enquanto os outros aceitam um bloco de código, isso quer dizer que você só deve utilizar eval como último caminho, eval sempre executará no contexto atual. O exemplo abaixo demonstra bem as diferenças entre as outras.

CODE
Cat = Class.new
Cat.class_eval do
   def from_class_eval
     "From Cat::class_eval"
   end
end
Cat.module_eval do
   def from_module_eval
     "From Cat::module_eval"
   end
end
Cat.instance_eval do
   def from_instance_eval
     "From Cat::instance_eval"
   end
end

cat = Cat.new

cat.instance_eval do
   def from_instance_eval
     "From cat.instance_eval"
   end
end

cat.from_class_eval     #=> "From Cat::class_eval"
cat.from_module_eval    #=> "From Cat::module_eval"
cat.from_instance_eval  #=> "From cat.instance_eval"

#Cat.from_class_eval     #=> NoMethodError
#Cat.from_module_eval    #=> NoMethodError
Cat.from_instance_eval  #=> "From Cat::instance_eval"


Declarações de classe

Quando se define uma classe em Ruby o código dentro da definição não é estático, podemos fazer comparações, usar estruturas de controle, etc... Assim como chamar métodos de suas super classes. É isso que iremos utilizar para dar um exemplo do que foi utilizado no Rails e o que pode se chamar de linguagem especifica de domínio (DSL). Olhe o exemplo abaixo e veja como é fácil criar uma "declaração", que na verdade é uma chamada de método:

CODE
class Product
  
   #Definindo um método de classe da classe que herda de Product chamado category
   def self.set_category(cat)
     #Definindo métodos dentro da metaclasse que serão da classe
     self.metaclass.class_eval do
       define_method(:category) do
         cat
       end
     end
   end  
  
end

class Camera < Product
   set_category "eletronicos"
  
end

Camera.category  #=> "eletronicos"

Assim como os métodos de classe das superclasses estarão disponíveis como se fossem declarações, os métodos das instâncias, que podem ser módulos ou classes, de Module ou Class também, lembrando que Class herda de Module. Olhe o exemplo abaixo que demonstra isso reescrevendo o atalho attr_accessor:

CODE
class Module
  
   def attr_accessor_personal(*attrs)
     attrs.each do |attr|
       attr = attr.to_s;
       #Definindo métodos dentro da classe que serão da instância
       self.class_eval do
         #Defininfo o getter -> objeto.atributo
         define_method(attr) do
           #Pegando a variável de instancia -> @atributo
           instance_variable_get("@"+attr);
         end
         #Defininfo o setter -> objeto.atributo = "valor"
         define_method(attr+"=") do |value|
           #Setando a variável de instancia -> @atributo = "valor"
           instance_variable_set("@"+attr, value);
         end
       end
     end
   end

end

class Car
   attr_accessor_personal :model, :factory
  
   def initialize(model, factory)
     @model, @factory = model, factory;
   end

end

car = Car.new "Enzo", "Ferrari"
car.model    #=> "Enzo"
car.factory  #=> "Ferrari"
car.model = "Gallardo"
car.factory = "Lamborghini"
car.model    #=> "Gallardo"
car.factory  #=> "Lamborghini"

Nos exemplos acima deu para perceber o quanto podemos modificar de classes e objetos.


Usando 'method_missing'

Uma das grandes utilidades de Ruby é method_missing, que permite executar algum código quando o objeto não responde a algum método. Isso simplifica e muito as coisas. No exemplo abaixo uma extensão à classe Hash.

CODE
class Hash
  
   alias_method :method_missing2, :method_missing;
  
   def method_missing(method, *args, &block)
     method = method.to_s;
     if method =~ /(.*)=$/ && self[$1]
       self[$1] = args[0];
     elsif self[method] && args.empty?
       self[method]
     else
       method_missing2(method, *args, &block);
     end
   end

end

hash = {"nome" => "Bernardo Rufino", "email" => "bermonruf@gmail.com"}

hash.nome   #=> "Bernardo Rufino"
hash.email  #=> "bermonruf@gmail.com"
hash.nome = "Alguem Silva"
hash.email = "mail@hotmail.com"
hash.nome   #=> "Alguem Silva"
hash.email  #=> "mail@hotmail.com"

Outro exemplo pode ser visto no XMLBuilder que cria documentos XML assim, e é usado no Rails.

Pra quem quiser ver os exemplos no editor de sua preferência pode baixar o arquivo contendo todos eles em metaprogramacao_em_ruby.rb.

Fontes:

Você gostou? Comente no fórum!

Mais recentes em Ruby

Iniciando no ruby
Por Vinik - Tutoriais by crusty...
Traduzir models
Por Bermonruf - Mensagens de erro e atributos...
Metaprogramação em ruby
Por Bermonruf - Nome: metaprogramação em ruby - autor: bernardo monteiro...
Sistema de arquivos e zip
Por Bermonruf - Criar, ler, deletar diretórios e arquivos...
Ruby básico
Por Bermonruf - From javascript to ruby...

Ver mais Artigos de Ruby.

Ver e retirar outras dúvidas no fórum Webly.

Alguns Direitos Reservados | RSS | O Fórum

Webly Portal e Fóruns - Internet + Humana | Design by ArthurHenrique.com