Webly: Metaprogramação em Ruby - Webly

Ir para

IMPORTANTE: Todos os tutoriais postados neste fórum irão automaticamente para o portal Webly. Contribua você também e faça parte da equipe de colaboradores que fazem a evolução da web. Obrigado.
Página 1 de 1

Metaprogramação em Ruby Avaliar tópico: -----

#1 Membro offline   Bermonruf Ícone

  • Bernardo Rufino
  • Ícone
Grupo:
Moderadores
Posts:
762
Cadastrado:
04-outubro 06
Location:
Rio de Janeiro, RJ
Interests:
Ruby On Rails, PHP, Javascript, Ajax, CSS, XHTML, DOM, XML

Postou 09 julho 2007 - 12:21

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.

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.

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:

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.

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.

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

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:

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.

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.

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

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.

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.

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:

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:

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.

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:
Blog: http://bermonruf.wordpress.com
0

Página 1 de 1


Resposta rápida

  • Diminuir tamanho
  • Aumentar tamanho
  

1 usuário(s) está(ão) lendo este tópico
0 membro(s), 1 visitante(s) e 0 membros anônimo(s)