Hoje fiquei vendo um exercício de probabilidade na aula de estatística, o exercício dizia o seguinte:
Uma moeda é lançada 10 vezes. Qual a probabilidade de:
a) Sair 5 vezes a face "cara".
b) Sair no máximo 9 vezes a face "cara".
Logo imaginei uma forma de descrever esta situação para o computador. Escrevi a seguinte frase:
objeto moeda possui faces(cara, coroa) quando jogar sortear faces sortear moeda
Pensando nesta declaração, codifiquei a seguinte declaração em ruby:
objeto "moeda" possui :faces => ["cara", "coroa"] quando "jogar" { sortear :faces }
Apartir do código acima, é possível executar o seguinte código:
moeda.jogar
Agora pensei, falta implementar o método, então vamos lá! Primeiro é necessário criar um método que permita eu declarar um objeto como:
objeto "moeda"
E logo que eu declarar o objeto, logo posso utiliza-lo livremente em ruby. Sem um toque de metaprogramação podemos verificar a seguinte situação:
[jonatas] ~ $ irb >> def objeto(nome) ; nome ; end => nil >> objeto 'moeda' => "moeda" >> moeda NameError: undefined local variable or method 'moeda' for main:Object
Não encontrou a variável local? E agora, como nascerá este método! Inicialmente criaremos um objeto anônimo a cada declaração de objetos. E manteremos uma lista dos objetos específicos.
Primeiro, iremos declarar uma váriavel que conterá todos os objetos que serão descritos.
$objetos = {} class Object def objeto(nome) $objeto = $objetos[nome] = Object.new end end
Após este passo, é necessário implementar o método “method_missing” que é responsável por saber a respeito das chamadas do sistema.
Este método é invocado, quando o método não é encontrado na classe, e recebe como parâmetros, um símbolo representando o nome do método, os parâmetros e um possível bloco.
O objetivo de implementar este método, é lidar com situações inesperadas como esta, que está buscando um método que não existe. Neste caso, iremos flexibilizar o uso da linguagem ruby, implementando uma mini-linguagem para descrever um objeto.
$objetos = {} class Object def objeto(nome) $objeto = $objetos[nome] = Object.new end def method_missing(nome, *args, &block) $objetos[nome.to_s] || super end end
Agora já é possível digitar:
objeto "moeda" moeda moeda.jogar # NoMethodError: undefined method 'jogar'
Para descrever o método jogar, é necessário implementar a sintaxe que cria novos atributos ao objeto descrito anteriormente.
def possui(atributos) atributos.each do |nome, valor| $objeto.instance_variable_set("@#{nome}",valor) end end
Depois que este elemento foi declarado é possivel declarar atributos para o objeto.
Dado que os atributos foram declarados, é necessário poder acessá-los também. Para tornar o acesso público, é necessário alterar a implementação do método “method_missing” para ler os atributos do objeto também.
def method_missing(nome, *args, &block) $objetos[nome.to_s] || $objeto.instance_variable_get("@#{nome}") || super end
O método acima, primeiramente verifica se existe um objeto com o nome, se não tenta buscar uma variável de instância para o objeto a seguir. Caso não encontre a variável então lança a excessão novamente.
objeto 'carro' possui 'portas' => 4 possui 'airbag' => 'duplo' carro.portas # => 4 carro.dirigir # NoMethodError: undefined method 'dirigir'
Agora é necessário tomar as ações para que possa “aprender” a dirigir o carro ou “jogar” a moeda. O próximo objetivo então é implementar o método quando determinada ação acontecer, então faça…
A deste método consiste em:
def quando(acao, &block) $objeto.class.send(:define_method, acao, &block) end
Agora, já é possível criar os métodos dinâmicamente para cada objeto. E através da sintaxe:
quando "jogar" { sortear :faces }
Desta forma já é possível utilizar a tão desejada sintaxe:
moeda.jogar
Abaixo segue o exemplo completo do código rodando:
$objetos = {} class Object def objeto(nome) $objeto = $objetos[nome] = Object.new end def method_missing(nome, *args, &block) $objetos[nome.to_s] || $objeto.instance_variable_get("@#{nome}") || super end def possui(atributos) atributos.each do |nome, valor| $objeto.instance_variable_set("@#{nome}",valor) end end def quando(acao, &block) $objeto.class.send(:define_method, acao, &block) end def sortear(atributo) atributo = $objeto.instance_variable_get("@#{atributo}") p atributo[rand(atributo.size)] end end objeto "moeda" possui :faces => ["cara", "coroa"] quando("jogar") { sortear(:faces) } 10.times { moeda.jogar }
Ruby é excepcional para trabalhar com DSL. O domínio específico da linguagem torna as tarefas mais simples e diretas. Isto se trata de expressividade, de melhorar a linguagem para estabelecer uma conversa mais direta e compreensiva. No lado da metaprogramação, torna-se simples de implementar as ideias propostas. A codificação e montagem deste post foi feito em paralelo e em um tempo satisfatório.
ps: se você se interessa por este assunto, leia também meu artigo sobre expressividade da linguagem em http://github.com/jonatas/artigo_elep.