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: