Saikuro, complexidade ciclomática para Ruby

November 12th, 2008 § 0 comments § permalink

Saikuro é uma analisador de complexidade ciclomática para Ruby. A instalação e uso do mesmo são bem simples e serão descritos aqui brevemente.

Instalação

Para instalar o Saikuro, basta rodar o comando gem:

~$ sudo gem install Saikuro

Esse comando instala a gem em si e um executável que pode ser usado na linha de comando.

Um programa de exemplo

Para demonstrar o uso da ferramenta, vamos utilizar um programa de referência. Esse programa será um simples transformador de objetos Ruby em hashes equivalentes e está listado abaixo, com os testes necessários:

require "test/unit"

class DocumentHasher

def initialize(object, options = {}) @object = object @options = options end

def hash hash = { :id => @object.id } if @options[:fields] @options[:fields].each do |field| hash[field] = @object.send(field) end end if @options[:extrafields] @options[:extrafields].each do |field| hash[field] = @object.send(field.tos + "field") end end hash end

def self.hash(object, options = {}) self.new(object, options).hash end

end

class DocumentHasherTest < Test::Unit::TestCase

Struct.new("HashExample", :id, :title, :author)

TITLEFIELD = "Test" AUTHORFIELD = "Author" EXTRA_FIELD = "Extra"

def setup @object = Struct::HashExample.new(1, TITLEFIELD, AUTHORFIELD) class << @object def extra_field "Extra" end end end

def testhashid result = DocumentHasher.hash(@object) assert_equal 1, result[:id] end

def testhashfields result = DocumentHasher.hash(@object, :fields => [:title, :author]) assertequal TITLEFIELD, result[:title] assertequal AUTHORFIELD, result[:author] end

def testhashextrafields result = DocumentHasher.hash(@object, :fields => [:title, :author], :extrafields => [:extra]) assertequal EXTRAFIELD, result[:extra] end

end

Salve esse arquivo para um arquivo chamado hasher.rb e rode-o com o comando abaixo para verificar sua execução:

~$ ruby hasher.rb

Utilização

Com esse programa, podemos rodar o Saikuro. O comando é simples:

~$ saikuro -c -t -y 0 -w 11 -e 16 -i hasher.rb -o report

Esse comando roda o analisador de complexidade com os seguintes parâmetros:

  • -t, para analisar tokens também
  • -y 0, para exibir a complexidade de todos os métodos
  • -w 11, para exibir warnings para métodos de complexidade superior a 11
  • -e 16, para exibir erros para métodos de complexidade superior a 16
  • -i hasher.rb, o arquivo a ser analisado (pode ser um diretório também)
  • -o report, o diretório onde gerar o relatório de saída

Resultado inicial

O resultado da execução acima pode ser visto, parcialmente, na imagem abaixo, que mostra os resultados somente para a classe em que estamos interessados:

Saikuro 1

Note que o método hash possui complexidade 5 que reflete os 4 comandos decisórios (2 if e 2 each) mais um ponto de saída pelo cálculo mais simples da complexidade. Os demais métodos, puramente lineares, possuem complexidade 1.

No diretório de resultado há também um arquivo que mostra a quantidade de tokens gerados no programa. Este relatório, com os níveis de aviso baixo, tende a reportar erros mesmo em condições consideradas normais.

Alterando o programa

Para fins de exemplo, podemos tentar reduzir a complexidade do método hash. Digamos que façamos a seguinte alteração:

require "test/unit"

class DocumentHasher
  
  def initialize(object, options = {})
    @object = object
    @options = options
  end
  
  def hash  
    hash = { :id => @object.id }
    build_fields(hash, @options[:fields] || {})
    build_extra_fields(hash, @options[:extra_fields] || {})
    hash
  end
  
  def self.hash(object, options = {})
    self.new(object, options).hash
  end
  
  protected
  
  def build_fields(hash, fields)
    fields.each do |field|
      hash[field] = @object.send(field)
    end
  end
  
  def build_extra_fields(hash, fields)
    fields.each do |field|
      hash[field] = @object.send(field.to_s + "_field")
    end
  end    
  
end

class DocumentHasherTest < Test::Unit::TestCase
  
  Struct.new("HashExample", :id, :title, :author)
  
  TITLE_FIELD = "Test"
  AUTHOR_FIELD = "Author"
  EXTRA_FIELD = "Extra"
  
  def setup
    @object = Struct::HashExample.new(1, TITLE_FIELD, AUTHOR_FIELD)
    class << @object
      def extra_field
        "Extra"
      end
    end
  end
  
  def test_hash_id
    result = DocumentHasher.hash(@object)
    assert_equal 1, result[:id]
  end

  def test_hash_fields
    result = DocumentHasher.hash(@object, 
      :fields => [:title, :author])
    assert_equal TITLE_FIELD, result[:title]
    assert_equal AUTHOR_FIELD, result[:author]    
  end

  def test_hash_extra_fields
    result = DocumentHasher.hash(@object, 
      :fields => [:title, :author], 
      :extra_fields => [:extra])
    assert_equal EXTRA_FIELD, result[:extra]
  end
  
end

Rodando o comando novamente, teríamos:

Saikuro 2

Como os testes demonstram, o programa funciona da mesma forma. E como o relatório de complexidade também demonstra, a complexidade do programa permanece a mesma no geral e a dos métodos diminuiu. O resultado é um programa mais legível e mais facilmente testável.

Conclusão

Embora métricas em si não tenham nenhum poder de tornar o código de um programa melhor, acompanhar a evolução das mesmas é uma ferramenta fundamental para garantir a qualidade do código. Para quem usa o Ruby, o Saikuro é uma ferramenta simples e rápida para isso. Para quem não usa, é fácil achar ferramentas que se adequam a outras linguagens.

Como de usual, sugestões, críticas e dúvidas são bem-vindos. No próximo artigo, ainda hoje, esfolando o seu código Ruby com mais ferramentas de análise de complexidade.

Rails Summit ’08

October 17th, 2008 § 0 comments § permalink

Como mais de quinhentos outros Railers do Brasil, participei ontem e ante-ontem do Rails Summit ’08. Foram dois dias intensos de bastante palestras e conversas no corredores e salões do Anhembi, e, como a maioria dos participantes, fiquei bem satisfeito com a experiência.

Esse foi o primeiro evento brasileiro de Rails de larga escala, contando com a participação de palestrantes da comunidade Rails internacional e para uma primeira edição ficou bem além das expectativas que qualquer um poderia ter tido. A organização foi impecável, tanto no que prometeu fazer quanto no que teve que resolver quando algumas coisas não saíram como previsto e o resultado final foi um evento de qualidade que nada ficou a dever a conferência do mesmo porte que já são realizadas para outras comunidades.

Outros blogueiros já falaram bastante sobre as palestras e acontecimentos e não vou me estender mais sobre o assunto. Para um outro apanhado geral, o livestreaming do evento que preparamos aqui na WebCo também mostra tudo que o pessoal andou falando e ainda está falando sobre o Rails Summit.

Finalizando, um mega-parabéns ao Akita pelo esforço demonstrado antes e durante o evento na promoção da comunidade aqui no Brasil.

Eu não…

May 8th, 2008 § 22 comments § permalink

  • Eu não escolheria Rails para escrever uma aplicação Web que consistisse, em sua maior parte, em processamento fora da interface com o usuário, ou cujo maior ponto fosse uma API;

  • Eu não escolheria Django para uma aplicação cujo domínio fosse extremamente complexo, com modelos dinâmicos, tabelas construídas on-the-fly, e funcionalidades similares;

  • Eu não escolheria Seaside para uma aplicação que consistisse em recursos individualizados, independentes e cujo referenciamento fosse essencial;

  • Eu não escolheria ASP.NET para um site inerentemente visual, cuja interface externa com o usuário fosse a parte essencial da aplicação;

  • Eu não escolheria Castle Monorail para aplicações de pequeno ou médio porte em que manutenção fosse mais importante do que o desenvolvimento de nova funcionalidade;

  • Eu não escolheria Ruby para aplicações de rede onde o potencial de instabilidade fosse maior do que o normal;

  • Eu não escolheria Python para um ORM ultra-adaptável;

  • Eu não escolheria ASP clássico ou PHP puro para coisa nenhuma.

Conversa com Randal L. Schwartz

April 30th, 2008 § 7 comments § permalink

Durante o FISL, eu tive a oportunidade de assistir a palestra sobre Seaside dada por Randal L. Schwartz, nome famoso nas comunidades livres, principalmente de Perl, e que agora está evangelizando Seaside e Smalltalk.

Perguntei se podíamos conversar um pouco e ele aceitou graciosamente. O resultado está abaixo, em uma tradução livre:

Conte-nos um pouco sobre o seu background, como e quando você começou a programar e o que você está fazendo hoje

Eu aprendi a programar sozinho quando tinha nove anos. Quando cheguei aos quinze, já estava na frente de uma classe, ensinando meus colegas, e escrevendo código sob contrato durante os fins de semana e sendo pago de verdade.

Trabalhei para várias empresas ao longo de oito anos antes de criar a Stonehenge em 1985. A Stonehenge cresceu ao longo dos anos; nós hoje podemos contar 17 das empresas no Fortune 100 como nossos clientes.

Hoje eu gasto a maior parte do meu tempo dando palestras e escrevendo, mas ainda projeto, crio e revejo código também. Também respondo questões gratuitamente durante uma hora por dia nas dezenas de listas, blogs e comunidades Web das quais participo.

Você é extremamente famoso na comunidade Perl, mas agora está advogando Smalltalk e Seaside. O que mudou? Quando você realmente começou a usar Smalltalk?
Eu comecei a usar Smalltalk antes de Perl ser inventada, em 1982. Já escrevi sobre a história no meu blog.
Quais são as vantagens de Smalltalk sobre outras linguagens tradicionais como Perl, Ruby ou Python, por exemplo?

Smalltalk tem uma sintaxe muito simples: eu posso ensinar a sintaxe completa a alguém em cerca de 20 minutos, e, de fato, a incluo em minhas palestras introduzindo as pessoas ao Seaside. As principais implementações Smalltalk (com exceção de GNU Smalltalk) também possuem IDEs maduros permitindo a fácil exploração do código, de modo que aprender as bibliotecas é somente uma questão de olhar a implementação e uso das mesmas

E isso também é um bônus: nós temos duas implementações comerciais (Cincom e GemStone/S) e duas abertas (Squeak e GNU Smalltalk), todas suportando Seaside. Isso permite que gerentes mais preocupados, que podem hesitar ao selecionar uma linguagem estritamente suportada por voluntários, escolherem entre dois provedores comerciais para suporte. Opções são sempre boas!

Você acha que Smalltalk finalmente vai alcançar status mainstream, isto é, ganhar aceitação do mercado em geral?

Bem, Smalltalk tinha status mainstream no meio dos anos 90, logo antes de Java entrar na história, pelo menos do que tange a firmas grandes de Wall Street e outras instituições que desejavam desenvolvimento rápido de GUIs para ficar à frente da competição.

Mas sim, eu acredito que Smalltalk está posicionada para entrar o mercado novamente como um grande player. Para mais detalhes, veja meu artigo sobre O Ano do Smalltalk.

Sua palestra tinha o título, “Seaside: Seu Próximo Framework Web”. O que há de tão interessante sobre o Seaside?

Eu gosto da forma como Seaside consegue abstrair tanto o fluxo de controle (ao longo de um eixo) e a representação (ao longo do outro eixo) com relativa facilidade. Seaside parece colocar as coisas certas relacionadas perto uma das outras. Eu também gosto do “depure uma página com erro dentro da própria página”: quando algum erro aconteceu, eu posso explorar a situação dentro do depurador normalmente, consertar o que está quebrado, limpar a bagunça, e continuar a execução da página como se nada houvesse acontecido.

Da mesm forma, a persistência tradicional em Rails é feita através do Active Record, que requer que objetos passem por um mapeador objeto-relacional para chegar em SQL. Seaside pode fazer a mesma coisa (via GLORP), mas possui soluções melhores pulando inteiramente o mapeamento, e usando coisas como Magma (OODBMS), que é aberto, ou algo como GemStone/S Virtual Machine, que é comercial. Quando você tira a camada ORM, voc&e ganha muita velocidade e um ambiente de programação bem mais confortável.

O que você vê no futuro do Seaside, e como esse futuro se compara ao dos outros frameworks?
A equipe do Seaside está agora no processo de refatorar e re-empacotar o Seaside para tornar a portabilidade ao longo de várias plataformas algo mais facilmente gerenciável e modular. Isso permitirá que cada desenvolvedor use somente as partes que desejar. Eu também estou vendo vários add-ons sendo criados, como o Pier CMS e várias APIs para coisas como Google Graphs e assim por diante.
Você acha que o mercado está pronto para o Seaside?
Sim. O Rails reabriu as discussões sobre o que fazer em um mundo após o Java, voltando para linguagens com late-binding como Perl, Python e Smalltalk. E Seaside é um framework maduro, ainda mais velho do que Rails. Somente não tão conhecido. Eu espero mudar isso.
Você já colocou algo em produção com o Seaside? Se sim, quais foram os desafios?
Eu estou trabalhando em alguns projetos agora, mas nada público. O desafio inicial foi a relativa falta de documentação, o que me forçou a gastar dois dias passando por todos os e-mails na lista de discussão. Fiquei mais informada, mas definitivamente com os olhos cansados. Eu espero devolver esse conhecimento escrevendo em meu blog e ajudando a responder questões no IRC e lista de discussão.
Você agora é parte do Squeak Foundation Board. Quais são seus planos para a Squeak Foundation?
Minha preocupação primária agora são questões de licenciamento, controle das releases e publicidade. Todas essas questões já estão sendo trabalhadas, é claro, mas todos somos voluntários e sempre estamos procurando mais voluntários para ajudar.
A Squeak Foundation tem algum plano para Seaside?
Nada formal, até onde eu sei. Entretanto, como Squeak é a plataforma primária para desenvolvimento do Seaside, eu tenho certeza de que Seaside será um componente principal nesse área.
Quais são os planos mais interessantes dentro do mundo Smalltalk/Seaside atualmente?
Bem, o que me envolve no momento é o GLASS (GemStone/Linux/Apache/Seaside/Squeak), uma solução da GemStone para que pessoas possam conseguir rodar Seaside rapidaemnte. Isso também envolve levar o pessoal da GemStone a criar uma solução comercial de custo zero, mas ainda funcional (mesmo que de forma mais limitada) do GemStone/S. Com essa versão gratuita, uma pessoa poderá construir um negócio, e quando o negócio exceder as capacidades da versão, migrar sem problemas para licenças maiores e ainda razoáveis. É um bela solução para uma VM Smalltalk sólida e comercialmente suportada com persistência e clustering já presentes.
E sobre o próximo ano do FISL? Como você conseguiu três dias inteiros para Smalltalk?
Bem, como eu disse, “tudo começou com algumas doses de caipirinhas…”
Quais são os seus planos para esses três dias? Você tem a intenção de trazer outros Smalltalkers?
Eu estou trabalhando com os organizadores do FISL e vários outros provedores de soluções e grupos dentro da comunidade Smalltalk para produzir uma mini-conferências. Espero ter treinamento inicial e avançado de Smalltalk, e vários tutoriais de Seaside. Expero que a conferência possa atrair um número significativo de desenvolvedores Smalltalk para o FISL pela primeira vez, e expor os demais participantes à linguagem, de modo que todos ganhem.

Muito obrigado, Randal, pela entrevista.

Testando agressivamente com Ruby

January 17th, 2008 § 0 comments § permalink

Um dos maiores problemas com o desenvolvimento guiado por testes é saber se você está testando o que realmente interessa. Muitas vezes, detalhes triviais de como um determinado trecho de código é implementando tornam os testes completamente inúteis sem que você perceba.

Um ferramenta cobertura, por melhor que seja, é inútil nesse momento porque ela pode dizer somente que o código foi executado mas não de que forma foi executado. Condições de inicialização e valores assumidos são especialmente perigosos no que tange a essa faceta de TDD.

Uma ferramenta que está se tornando essencial para mim nos últimos tempos é o Heclke. O Heckle é um testador de mutações. O que ele faz é modificar seu código de maneiras inusitadas e rodar os testes novamente para ver se os mesmos ainda estão passando. Não é uma ferramenta perfeita–há momentos em que as mutações realmente são inúteis ou em que os testes entram em um loop infinito–mas já faz um trabalho enorme em identificar potenciais caminhas não tratados no seu código.

A instalação é simples:

sudo gem install heckle

E o uso também é bem simples:

heckle class_name [method_name] -t [test_file]

É possível usá-lo também com RSpec:

spec spec_file --heckle class_name[#method_name]

O nome do método é opcional e se não for informado, todos os métodos da classe serão modificados. Os testes podem levar um tempo maior para rodar nesse caso mas os resultados são bem positivos.

Para um exemplo mais detalhado, leia o texto do anúncio original do Heckle. O programa atualmente é bem mais sofisticado mas o princípio é o mesmo.

Domain Specific Languages: Considerações Finais

January 11th, 2008 § 0 comments § permalink

Este é o quinto artigo em uma série sobre DSL, ou Linguagens Específicas de Domínio. O primeiro artigo introduziu o assunto explicando o que é uma DSL e como elas podem melhorar e simplificar o código de uma aplicação. O segundo artigo mostrou o processo de transformação de um problema usual em um problema resolvido por uma DSL. O terceiro artigo mostrava a implementação de uma DSL simples, usando as características básicas de meta-programação do Ruby. Por fim, o quarto artigo mostrava uma implementação mais complexa ilustrando outras técnicas úteis.

Neste último artigo, é explorar rapidamente alguns temas relacionados e mostrar o que está vindo por aí. Com a popularidade do Ruby e outras linguagens dinâmicas e funcionais, o assunto está se tornando cada vez mais estudado e vale a pena acompanhar o que está acontecendo.

Quando usar uma DSL

Muitos programadores, em seu primeiro contato com o tema, decidem que todo e qualquer problema podem ser resolvidos com uma DSL. Com o ditado diz, se você tem um martelo tudo se parece com um prego. O resultado são programas povoados de pequenas linguagens que precisam de uma grande quantidade de código para conversarem em si.

Embora seja possível–e em alguns casos interessante–coordenar interfaces entre uma DSL e outra, isso reduz consideravelmente os benefícios de isolar um problema, reduzir o seu foco e isolar a complexidade do código relacionado. Quando se começa a pensar na interface que uma DSL vai prover para outra DSL, a questão pode ficar complicada.

Para que está começando, o uso de DSL vai ser mais interessante em duas áreas: envolver código legado em interfaces mais acessíveis e refatorar seqüências complexas em algo mais gerenciável. Não é que uma DSL só sirva para essas duas tarefas. Mas, na minha experiência, é onde estão os maiores ganhos para quem está começando.

A partir daí, progredir para a elaboração de ferramentas internas mais complexas e finalmente chegar ao uso de ferramentas externas é um caminho mais tranqüilo.

Interfaces fluentes

Um termo que recentemente está sendo associado ao uso de DSL é interfaces fluentes. Como Martin Fowler–um dos criadores do termo–menciona nesse artigo, o termo é basicamente intercambiável com DSL e representa uma forma de concatenar chamadas em algo mais fluido e legível.

Um exemplo bem simples disso são os manipuladores de data no Rails:

(4.months + 5.years).from_now

Esse código pode ser lido naturalmente e seu intento é evidente de imediato. O objetivo de uma interface fluente é justamente realizar essa transição–o código imperativo e fixo é convertido em algo flexível e de fácil leitura.

Para linguagens que não possui capacidade específica de meta-programação, interfaces fluentes podem ser a maneira mais fácil de implementar uma DSL. Em C# 2.0, por exemplo, seria fácil dizer algo do tipo:

broker.ProcessFile(fileName).Using(MyService).
  MapColumn("A1").ToField("Name").FormattedAs(typeof(String)).
  MapColumn("A2").ToField("Date").FormattedAs(typeof(DateTime)).
    WithOptions("Year").As(DateTime.Today).
  MapColumn("A3").ToField("OrderId").FormattedAs(typeof(Integer)).
    WithOptions("Unique").As(True).
    WithOptions("EagerLoading").As(False)

O código é necessariamente mais verboso do que seria se a meta-programação fosse mais evidente na linguagem mas ainda é mais legível do que se estivesse quebrado em múltiplas classes e implementações.

Se você precisa usar uma linguagem que não é tão flexível, buscar linguagens fluentes pode ser uma estratégia bem interessante para a implementação de uma DSL.

Testando

Como a implementação de uma DSL geralmente envolve muita mágica, por assim dizer, testar a mesma pode se tornar algo complicado quando o escopo de execução dos blocos está sendo modificado ou é dinâmico–principalmente em linguagens onde os próprios mecanismos de teste implementam uma DSL com toda a dinâmica adjacente.

Nesses casos, a melhor estratégia não é estar a própria DSL, mas testar o que ele está gerando. Pode parecer que isso não dá um cobertura interessante, mas há um equivalência óbvia entre as duas coisas. Escolher os exemplos apropriados não é difícil e é possível exercitar todos os aspectos da geração simples observando o que ela está fazendo.

Quando a DSL possui um produto bem específico e final, a situação se torna ainda mais simples e o teste desse produto é uma validação automática.

Finalmente, o código que usa a DSL pode estar testado naturalmente já que o contexto encapsulado da DSL é isolado nesse caso.

O futuro próximo

Vários assuntos e tecnologias estão surgindo em torno do potencial de linguagens específicas. Entres eles estão geradores baseados em DSL com modificações two-way, language workbenches e programação orientada a linguagens.

Geradores de código estão entre as ferramentas mais vilipendiadas na história da programação, embora a sua utilidade seja inquestionável. De fato, usuários de linguagens cujo afinidade com DSL é patente, como Smalltalk, sempre utilizaram geradores de código em profusão e sem efeitos detrimentais para o código.

O maior problema com geração de código, entretanto, é que poucas vezes a geração é reversível. Isto, modificações no código gerado não são refletidas para o template usado. Uma DSL pode resolver isso servindo tanto de modelo como de código final, permitindo que haja uma correspondência direta entre as duas esferas. Esse é o tipo de problema que será mais e mais resolvido por linguagens específicas.

Language Workbench, por sua vez, é um novo termo cunhado por Martin Fowler para representar uma nova classe de ferramentas que permitem manipular uma DSL de maneira mais formal e persistente. De certa forma, eles estão para DSL como ferramentas de modelagem estão para UML diferindo, é claro, pela extrema flexibilidade. O uso de language Workbenches permite desenhar, testar e documentar uma DSL de uma maneira similar à criação de código.

Obviamente esse tipo de ferramenta ainda está em sua infância mas é algo que também será mais e mais utilizado nos próximos anos. O artigo de Fowler é uma fonte primária de informações e vale a pena se lido inteiramente–até porque fornece uma boa visão dos temas relacionados.

Finalmente, programação orientada a linguagem é outro assunto que está ganhando proeminência nos últimos meses. Originalmente, o termo se referia à mesma classe de soluções representada por linguagens específicas de domínio mas atualmente está se tornando um sinônimo para linguagens sintática e semanticamente flexíveis.

Isso e algo que linguagens como Lisp e Scheme vem fazendo há décadas mas que agora está sendo também traduzido para linguagens mais em voga como JavaScript e Ruby. Um bom exemplo disse é OMeta, uma sub-linguagem (no sentido de estar incorporada a outra) para extensões gramaticais.

Fechando o ciclo

Terminamos aqui a nossa série sobre DSL e esperamos que o tempo que você empregou lendo os textos tenha sido proveitoso, ajudando a completar o seu conhecimento. Quaisquer dúvidas, sugestões e correções podem ser deixadas nos comentários associados às entradas e os mesmos serão incorporados ao texto de acordo com as possibilidades.

Domain Specific Languages: Implementando mágica

January 10th, 2008 § 0 comments § permalink

Este é o quarto artigo em uma série sobre DSL, ou Linguagens Específicas de Domínio. O primeiro artigo introduziu o assunto explicando o que é uma DSL e como elas podem melhorar e simplificar o código de uma aplicação. O segundo artigo mostrou o processo de transformação de um problema usual em um problema resolvido por uma DSL. Finalmente, o terceiro artigo mostrava a implementação de uma DSL simples, usando as características básicas de meta-programação do Ruby.

Neste quarto artigo, mostraremos como implementar uma DSL um pouco mais complexa usando características adicionais de meta-programação do Ruby como a method_missing e proxies. Os exemplos serão necessariamente simples para não tornar o artigo muito extenso mas mostrarão o que é preciso para utilizar tais.

O problema

Para demonstrar as características acima, vamos utilizar o mesmo problema de definir regras para um jogo de Eleusis mas com o seguinte formato:

r1 = rule "Rule 1" do
  
  black means { its.suit.is_clubs | its.suit.is_spades }
  red means { its.suit.is_diamonds | its.suit.is_hearts }
  
  even means { its.value.is_even }
  odd means { its.value.is_odd }

  odd after { black & black }
  even after { red & red & red }
  
end

r1.run("2 of clubs", "3 of spades", "ace of clubs", "2 of hearts", 
       "2 of diamonds", "3 of hearts", "queen of clubs", 
       "ace of spades")

Note que agora estamos usando uma linguagem ainda mais próxima de uma definição verbal. Obviamente, vários elementos do próprio Ruby permanecem já que existem limites para o que pode ser modificado na linguagem básica.

Esse exemplo, como o anterior, não é capaz de processar todas as regras possíveis do jogo, e não possui qualquer recuperação de erro. Apesar disso, ele serve para ilustrar os conceitos que podem tornar uma DSL ainda mais efetiva na representação de seu problema.

Implementação

Como a classe Card que declaramos no arquivo anterior não sofreu qualquer mudança, não reproduziremos o seu código aqui.

Um detalhe de implementação geralmente associado com o desenho de qualquer DSL que faz uso de proxies ou interceptação de métodos é uma espécie de classe que chamamos de blank slate. Essas classes não possuem qualquer método de instância além do básico para o seu funcionamento interno dentro do Ruby.

O uso dessas classes atende dois propósitos dentro do desenho de uma DSL. Primeiro, permite identificar de uma forma mais rápida e precisa erros de interceptação de métodos, especialmente quando operadores estão sendo sobrepostos. Segundo, permite limpar quaisquer métodos adicionais que tenham sido acrescentados por outras bibliotecas reduzindo o risco de colisões.

A implementação que usaremos para a nossa class blank slate é a seguinte:

class BlankSlate
  
  KEEPER_METHODS = %w(__id__ __send__ inspect 
    class instance_eval hash)
  
  (instance_methods - KEEPER_METHODS).each do |v|
    next if v[-1] == ?? || v[-1] == ?!
    undef_method(v)
  end
  
end

Com exceção dos métodos essenciais, a classe acima não define qualquer outro método de instância. Sendo assim, qualquer classe derivada da mesma começara somente com os métodos mais básicos possíveis.

Com essa classe podemos começar a nossa implementação. O nosso arquivo básico, derivado do exemplo anterior e com as modificações necessárias da DSL, é o seguinte:

require "card"

class BlankSlate
  
  KEEPER_METHODS = %w(__id__ __send__ inspect
    class instance_eval hash)
  
  (instance_methods - KEEPER_METHODS).each do |v|
    next if v[-1] == ?? || v[-1] == ?!
    undef_method(v)
  end
  
end

class Rule < BlankSlate
  
  def initialize(name, &block)
    @name = @name
    @groups = {}
    @paths = []
    instance_eval(&block)
  end

  def run(*specs)
    cards = specs.collect { |spec| Card.from_string(spec) }
    rules = cards.zip(@paths * ((cards.length / @paths.length) + 1))
    rules.each_with_index do |pair, i|
      unless @groups[pair.last].include?(pair.first)
        puts "Rule failed at item #{i + 1}: " +
        "#{pair.first} did't match group #{pair.last.to_s}"
        return
      end
    end
    puts "Rule matched all cards."
  end

end

def rule(name, &block)
  Rule.new(name, &block)
end

r1 = rule "Rule 1" do
  
  black means { its.suit.is_clubs | its.suit.is_spades }
  red means { its.suit.is_diamonds | its.suit.is_hearts }
  
  even means { its.value.is_even }
  odd means { its.value.is_odd }

  odd after { black & black }
  even after { red & red & red }
  
end

r1.run("2 of clubs", "3 of spades", "ace of clubs", "2 of hearts", 
       "2 of diamonds", "3 of hearts", "queen of clubs", 
       "ace of spades")

O primeiro passo agora é implementar o método means que define as cartas que compõem uma regra. Esse método recebe um bloco com um método interno chamada its que pode ser usado para definir conjuntos de cartas através de expressões simples.

Podemos ter, como mostrado acima, black means { its.suit.is_clubs | its.suit.is_spades } para dizer “preto significa que seu naipe é copas ou espadas” e assim por diante. E, como veremos no resto da implementação, a sintaxe permite o uso de | e & para representar, respectivamente, o operador lógico ou e o operador lógico e.

A implementação do método é simples, sendo simplesmente uma delegação para a execução do próprio bloco:

class Rule < BlankSlate
  
  # ...
  
  protected

   def means(&block)
     instance_eval(&block)
   end
  
end

A execução real acontece através do método its, que é implementado assim:

class Rule < BlankSlate
  
  # ...
  
  protected

  def its
    FilterProxy.new
  end
  
end

Mais uma vez, temos uma delegação, agora para uma classe que servirá com um proxy para a invocação dos métodos necessários. Essa classe é implementada como descrito abaixo:

class FilterProxy < BlankSlate
  
  attr_reader :cards
  
  def |(proxy)
    @cards += proxy.cards
    self
  end
  
  def &(proxy)
    @cards.reject! { |card| !proxy.cards.include?(card) }
    self
  end

  protected
  
  def method_missing(name, *args)
    if [:suit, :face, :value].include?(name)
      @message = name
      self
    elsif name == :is_any_of
      @cards = Card.all.select { |card| 
        args.include?(card.send(@message)) 
      }
      self
    elsif name.to_s =~ /^is_(.*)$/
      if $1 == "odd"
        @cards = Card.all.select { |card| 
          card.send(@message) % 2 != 0 
        }
      elsif $1 == "even"
        @cards = Card.all.select { |card| 
          card.send(@message) % 2 == 0 
        }
      else
        @cards = Card.all.select { |card| 
          card.send(@message) == $1.to_sym 
        }
      end
      self
    else
      super
    end
  end
  
end

A classe FilterProxy possui duas atribuições: implementar a união e interseção de regras–através dos operadores & e |–e interceptar as mensagens emitadas na criação de critérios de grupo.

A união e interseção são simples. Elas são feitas entre duas instâncias da classe FilterProxy e retornam a primeira delas, alterada com a operação em questão. Como estamos descartando os objetos no bloco, isso não é um problema embora talvez seja mais interessante implementar a criação de uma classe pura se termos a intenção de filtrar mais extensivamente. Como estamos implementando uma seqüência simples, não vamos nos importar com esse detalhe agora.

O método method_missing é que implementa o grosso do trabalho, interceptando os métodos que são necessários para fazer os filtros funcionarem.

A primeira condição verifica se a mensagem é uma que a classe Card response. Se sim, a mensagem é salva para o próximo passo.

A segunda condição verifica se a mensagem é o método is_any_of que, com base na mensagem anteriormente salva e uma série de possíveis respostas, verifica quais cartas atendem a esse critério.

Finalmente, mensagens no formato is_<name> (como is_odd ou is_clubs) são implementadas de maneira similar, com um bloco que verifica se a resposta necessária atende a mensagem que foi passada.

Caso a mensagem não seja interceptada, o comportamento padrão é invocado, delegando a execução para a classe pai que, no caso, simplesmente faz o tratamento de erros.

A próxima implementação é o próprio método method_missing na class Rule, que lidará com os nomes dos grupos que estão sendo declarados. Essa implementação inicial fica da seguinte forma:

class Rule < BlankSlate
  
  # ...
  
  protected

  def method_missing(name, *args)
    if args.first.kind_of?(FilterProxy)
      @groups[name] = args.first.cards
    else
      super
    end
  end
  
end

Essa primeira implementação verifica se o primeiro argumento passado para uma mensagem qualquer é uma instância de FilterProxy. Caso seja, um grupo é definido com as cartas selecionadas pelo filtro. Qualquer outro desvio no processamento de mensagens gera um erro nesse momento.

Agora é hora de implementar o método after que também usa um bloco. A implementação é idêntica à do método means:

class Rule < BlankSlate
  
  # ...
  
  protected

  def after(&block)
    instance_eval(&block)
  end

end

Mais uma vez, estamos delegando o processamento à interceptação de mensagens. Para que isso funcione, precisamos alterar o método method_missing, que ficará da seguinte forma:

class Rule < BlankSlate
  
  # ...
  
  protected

  def method_missing(name, *args)
    if @groups[name]
      if args.size == 0
        GroupProxy.new(name)
      else
        @paths += args.first.sequence
        @paths << name
      end
    elsif args.first.kind_of?(FilterProxy)
      @groups[name] = args.first.cards
    else
      super
    end
  end

end

O código acima verificar se um grupo existe com aquela mensagem. Caso o grupo não exista, o processamento anterior é mantido. Se, entretanto, o grupo existir, temos duas respostas possíveis.

Primeiro, o método foi invocado sem argumentos. Isso significa que ele está sendo usado em uma definição da seção after, como pode ser visto pela regra. Nesse caso, um proxy é criado para tratar do processamento do bloco.

Segundo, o método foi invocado com argumentos. Isso significa que o mesmo está sendo usado como prefixo para uma regra after. Nesse caso, os argumentos são coletados para formar as regras que serão processadas. Como a regra define condições inversas, os grupos dentro do bloco são inseridos em primeiro lugar seguidos pelo grupo cujo nome é a própria mensagem.

A implementação da classe GroupFilter é trivial e pode ser vista abaixo:

class GroupProxy < BlankSlate
  
  attr_accessor :sequence
  
  def initialize(*sequence)
    self.sequence = sequence
  end
  
  def &(proxy)
    GroupProxy.new(*(@sequence + proxy.sequence))
  end
  
end

Temos duas situações possíveis. Primeiro, a inicialização, que simplesmente recebe uma série de nomes de grupos a ser mantida. Segundo, a união, que combina o conjunto atual com o conjunto sendo passado para formar um novo conjunto que é retornado em seguida.

Com isso nossa implementação está completa e pode ser utilizada na criação de regras razoavelmente complexas. É possível, por exemplo, definir grupos no seguinte formato:

black_and_even means { 
  (its.suit.is_clubs | its.suit.is_spades) & 
  its.value.is_even 
}

Como é fácil observar, é possível obter uma considerável expressividade mesmo com um código razoavelmente simples. Embora, obviamente, seja necessário muito mais do que isso para implementar qualquer regra Eleusis possível, o código acima já ilustra um caminho possível com uma legibilidade boa em relação ao domínio que a DSL trata.

Com isso encerramentos este exercício. No próximo artigo, algumas considerações finais sobre o que está sendo feito atualmente no campo e como resolver alguns problemas comum.

Domain Specific Languages: Uma implementação simples

January 9th, 2008 § 0 comments § permalink

Este é o terceiro artigo em uma série sobre DSL, ou Linguagens Específicas de Domínio. O primeiro artigo introduziu o assunto explicando o que é uma DSL, como elas podem melhorar e simplificar o código de uma aplicação e, finalmente, mostrando alguns exemplos usados pela maioria dos desenvolvedores. O segundo artigo mostrou o processo de transformação de um problema usual em um problema resolvido por uma DSL e a simplificação resultante da mudança.

Neste terceiro artigo, mostraremos como implementar uma DSL simples usando Ruby e quais são os passos necessários para que a mesma represente o intento do que estamos querendo fazer. Como esse é um exemplo inicial, somente os passos mais básicos da criação de um DSL serão mostrados. Em artigos posteriores, mostraremos outras técnicas mais avançadas.

A DSL que criaremos será baseada na descrição de regras do meu jogo de cartas preferido, Eleusis. Eleusis é um jogo que não depende de qualquer tipo de sorte e procura simular o processo indutivo de descobertas científicas. Funciona bem para grupos de três a oito pessoas embora o grupo ideal esteja entre quatro e cinco jogadores.

Em linhas gerais as regras são as seguintes:

  • A cada rodada, um jogadores diferente é o carteador. O jogo termina quando todos jogadores tiverem passado pelo papel de carteador;
  • O carteador inventa uma regra secreta que os outros jogadores devem descobrir;
  • Cada jogador que não o carteador recebe quatorze cartas iniciais;
  • O carteador coloca a primeira carta do jogo, que deve atender à primeira carta de sua regra;
  • Os outros jogadores se alteram colocando uma carta que supõem atender a regra;
  • Se acertarem, a carta fica após as outros cartas já jogadas;
  • Se errarem, são penalizados com duas cartas extras, e a carta errada é colocada perpendicularmente à ultima carta jogada;
  • A rodada termina quando um dos jogadores fica sem cartas (supostamente porque descobriu a regra e conseguiu descartar todas as castas que tinha em mãos);
  • O jogo termina quando todos jogadores tiverem ocupado a posição de carteador.

As regras inventadas podem variar de simples a complexas. Uma regra simples seria, por exemplo, jogar uma carta de copas seguida por uma de ouros seguida por uma de paus seguida por uma de espadas e assim por diante. Uma regra complexa seria jogar uma carta vermelha se a última foi par ou preta se a última foi ímpar. Uma bom carteador sabe equilibrar a complexidade de suas regras.

Mas, chega de explicar o jogo. A DSL que criaremos será uma linguagem para descrever as regras que podem ser criadas no jogo, descrevendo o intento da mesma através de construções simples. A regra também terá um método que, ao receber um grupo de cartas, dirá se esse grupo se conforma à regra.

Uma regra de exemplo poderia ser algo da forma abaixo:

r1 = rule "Rule 1" do
  
  group :black, :is => [:clubs, :spades]
  group :red, :is => [:diamonds, :hearts]
  
  group :even do |card|
    card.value % 2 == 0
  end
  
  group :o dd do |card|
    card.value % 2 != 0
  end

  get :black, :black
  get :o dd
  get :red, :red, :red
  get :even
  
end

Para fins desse exercício não usaremos nenhum grupo pré-definido ou suportaremos regras com recombinações, ou seja, regras que dependem de valores anteriores de cartas jogadas.

Depois que a regra está definida, ela pode ser usada para verificar um grupo qualquer de cartas. Por exemplo, poderíamos usar algo assim:

r1.run("2 of clubs", "3 of spades", 
       "ace of clubs", "2 of hearts")

Ao rodar esse teste, seremos informados se as cartas atendem à regra ou qual a primeira delas a falhar e o por quê.

Como você pode perceber ao ler o trecho que código que representa a regra, a mesma soa quase como uma descrição textual do que está acontecendo. Obviamente, como a sintaxe de uma linguagem qualquer não pode ser completamente flexível, algumas partes da definição não serão tão bem especificadas, mas o ganho em legibilidade é óbvio.

Da mesma forma, o código acima atende aos critérios de uma DSL:

  • Ele resolve um único problema, a descrição de uma regra do jogo
  • Ele reduz o foco do problema ao descrever a regra de uma forma mais intuitiva
  • Ele esconde o código complexo por trás de métodos simples

A criação da DSL em sim pode ser agora quebrada em alguns passos simples. Antes de começar, assumiremos, porém, que a seguinte classe representa as cartas que são usadas pela regra (partes do código foram formatadas diferentemente do comum em Ruby para se encaixar melhor do texto):

SUITS = [:clubs, :diamonds, :spades, :hearts].freeze

FACES = [:ace, :two, :three, :four, :five, :six, :seven, :eight,
         :nine, :ten, :jack, :queen, :king].freeze

class InvalidCardError < StandardError; end

class Card

  include Comparable

  def initialize(face, suit)
    @value = FACES.index(face) + 13 * SUITS.index(suit)
  rescue
    raise InvalidCardError
  end

  def face
    FACES[@value % 13]
  end

  def suit
    SUITS[@value / 13]
  end
  
  def value
    FACES.index(face) + 1
  end

  def eql?(card)
    @value == card.instance_variable_get(:@value)
  end

  def <=>(card)
    @value <=> card.instance_variable_get(:@value)
  end
  
  def to_s
    "#{face} of #{suit}"
  end

  def self.all
    @@all_cards ||= SUITS.inject([]) { |m, s| 
      m + FACES.collect { |f| Card.new(f, s) } 
    }
  end
  
  def self.from_string(spec)
    spec.downcase!
    if spec.length == 2
      parts = spec.split(//)
    else
      parts = spec.split("of").collect { |p| p.strip }
    end
    if parts.first =~ /\d+/
      face = FACES[parts.first.to_i - 1]
    elsif ["a", "j", "q", "k"].include?(parts.first)
      face = FACES.detect { |face| face.to_s[0,1] == spec[0,1] }
    else
      face = parts.first.to_sym
    end
    if parts.last.length == 1
      suit = SUITS.detect { |suit| suit.to_s[0,1] == parts.last }
    else
      suit = parts.last.to_sym
    end
    Card.new(face, suit)
  end

end

Queremos representar a nossa regra com uma classe que possa ser passada adiante e cujos métodos possam ser acessados por qualquer código interessado nos mesmos.

Nosso código inicial, então, seria algo assim:

require "card"

class Rule
  
  def initialize(name)
  end
  
  def group(name, items = nil)
  end
  
  def get(*groups)
  end
  
  def run(*specs)
    puts "Not implemented"
  end
  
end

r1 = Rule.new("Rule 1")

r1.group(:black, [:clubs, :spades])
r1.group(:red, [:diamonds, :hearts])

r1.group(:even) do |card| 
  card.value % 2 == 0
end

r1.group(:odd) do |card| 
  card.value % 2 != 0
end

r1.get(:black, :black)
r1.get(:odd)
r1.get(:red, :red, :red)
r1.get(:even)

r1.run("2 of clubs", "3 of spades", 
       "ace of clubs", "2 of hearts")

Ainda não tem muito a ver com o que queremos, mas já é um começo. Podemos fazer o código funcionar implementado os métodos necessários.

O método initialize ficaria assim, inicializando as variáveis internas da classe:

class Rule
  
  def initialize(name)
    @name = name
    @groups = Hash.new([])
    @path = []    
  end

  # ...

end

O próximo método é get, igualmente simples, guardando os grupos que serão rotacionados para formar a regra:

class Rule
  
  def get(*groups)
    @path += groups
  end

  # ...

end

O método group é um pouco mais elaborado já que envolve duas interfaces diferentes:

class Rule
  
  def group(name, items = nil)
    if items.kind_of?(Array)
      @groups[name] = []
      items.each do |group|
        @groups[name] += Card.all.select { |card|
          (card.suit == group) ||
          (card.face == group) 
        }
      end
    elsif block_given?
      @groups[name] = Card.all.select { |card| yield card }
    end    
  end

  # ...

end

A primeira parte do método verifica se o parâmetros items foi passado. Se o mesmo é um Array contendo uma combinação de naipes ou faces, o código procura no grupo de todas as cartas possíveis as que possuem o naipe ou face passado. As cartas recuperadas são então guardadas como um grupo específico. Se o parâmetro item não foi passado e um bloco existe, o bloco é usado sobre o mesmo grupo de todas as cartas para escolher aquelas que atendem ao critério do bloco.

Finalmente temos o método run que define o processamento da regra em si:

class Rule
  
  def run(*specs)
    cards = specs.collect { |spec| Card.from_string(spec) }
    rules = cards.zip(@path * ((cards.length / @path.length) + 1))
    rules.each_with_index do |pair, i|
      unless @groups[pair.last].include?(pair.first)
        puts "Rule failed at item #{i + 1}: " +
        "#{pair.first} did't match group #{pair.last.to_s}"
        return
      end
    end
    puts "Rule matched all cards."
  end

  # ...

end

A implementação segue a seguinte lógica:

  • As descrições das cartas passadas são convertidas nas cartas equivalentes.
  • Essas cartas são pareadas com as posições equivalentes na regra. Como a regra pode ser menor do que o número de cartas passadas, as regras são duplicadas até que forem uma lista com o mesmo comprimento das cartas. O método zip é usado então para fazer o pareamento final.
  • Para cada par, a carta é verificada para ver se existe dentro do grupo pareado. Se a carta existe, o processamento continua. Se não, o processamento é abortado com a mensagem necessária.

Se você roda o programa agora, verá que o mesmo funciona e que você pode criar regras diferentes e verificá-las contra qualquer conjunto de cartas.

O objetivo agora é transformar o código acima em algo mais legível, que se aproxime mais de uma linguagem de definição de regras.

Esse processo é surpreendentemente simples. O primeiro passo é executar o código de definição dentro de um bloco. Por mais trivial que isso seja, a mudança dá a impressão de agrupamento ao código e o torna mais interessante. A mudança necessária é a seguinte:

class Rule
  
  def initialize(name)
    @name = name
    @groups = Hash.new([])
    @path = []    
    yield self
  end

  # ...

end

O método initialize agora requer um bloco que pode ser usado para encapsular as chamadas aos métodos internos da classe. Nesse ponto pode ser interessante mover os métodos get e group para a seção protected para evitar acesso externo aos mesmos. A chamada a yield invoca o bloco passado ao método e dá ao mesmo como parâmetro a própria instância sendo criada.

Com a mudança acima, a criação da regra pode ser mudada para:

r1 = Rule.new("Rule 1") do |r|

  r.group(:black, [:clubs, :spades])
  r.group(:red, [:diamonds, :hearts])

  r.group(:even) do |card| 
    card.value % 2 == 0
  end

  r.group(:odd) do |card| 
    card.value % 2 != 0
  end

  r.get(:black, :black)
  r.get(:odd)
  r.get(:red, :red, :red)
  r.get(:even)

  r.run("2 of clubs", "3 of spades", 
    "ace of clubs", "2 of hearts")
      
end

Já podemos ver que o código se assemelha mais a uma linguagem separada embora o uso do parâmetro não seja tão interessante.

Para conseguir eliminar o parâmetro precisamos de um pouco de mágica que consiste em invocar o código dentro do bloco como se ele fosse parte da própria classe. Embora yield também invoque o bloco, a invocação é realizada no contexto em que o bloco foi criado, ou seja, fora da classe. Se conseguirmos invocar o bloco dentro da classe, não precisaremos referenciar a instância e poderemos remover o código desnecessário.

class Rule
  
  def initialize(name, &block)
    @name = name
    @groups = Hash.new([])
    @path = []    
    instance_eval(&block)
  end

  # ...

end

Para entender a mudança, precisamos entender duas outras coisas. Primeiro, que block é uma parâmetro de bloco. Em Ruby, blocos de código são considerados variáveis passíveis de manipulação tanto como qualquer outra valor primitivo ou instâncias de classe. De fato, um bloco é nada mais que uma instância da classe Proc. Como tal, possuem não só o código associado como métodos que podem ser invocados sobre esse código. Para explicar isso, veja o seguinte código:

p = Proc.new { |a, b| a + b }
puts p.call(1, 2)

O código acima criar um bloco que recebe dois parâmetros e devolve a sua soma. Logo em seguida o bloco é invocado com dois parâmetros e imprime o resultada do invocação. Como é fácil perceber, blocos também são variáveis de primeira classe tanto quanto qualquer outro tipo. Sendo assim, eles podem ser passados como parâmetros que é exatamente o que estamos fazendo em nosso código.

Como blocos geralmente são passados como parâmetros escondidos, pelo uso das palavras-chave do...end ou {...}, o Ruby usa &amp; para expor o bloco. O significado de &amp; é o seguinte: pegue o bloco de código passado como parâmetro para o método, coloque-o na variável cujo nome está após o &amp; e não o execute até que o mesmo seja invocado implicitamente. O método yield, ao contrário, não precisa da variável e simplesmente executa o bloco quando usado.

No caso do método initialize estamos pegando esse bloco passado como parâmetro e transferindo sua execução para o método instance_eval. Esse método faz exatamente o que diz: recebe um bloco de código e o executa como se o bloco fosse local à classe. Mais uma vez precisamos de &amp; para indicar que o bloco será passado como parâmetro e não explicitamente através de do...end ou {...}.

Com essa mudança, nossa regra fica assim:

r1 = Rule.new("Rule 1") do

  group(:black, [:clubs, :spades])
  group(:red, [:diamonds, :hearts])

  group(:even) do |card| 
    card.value % 2 == 0
  end

  group(:odd) do |card| 
    card.value % 2 != 0
  end

  get(:black, :black)
  get(:odd)
  get(:red, :red, :red)
  get(:even)

  run("2 of clubs", "3 of spades", 
     "ace of clubs", "2 of hearts")
      
end

Estamos agora bem próximos do resultado final. Precisamos apenas de alguns ajustes.

Podemos, para começar esses ajustes, criar um método para envolver a chamada à classe e simplesmente retorná-la. Fazemos isso com:

def rule(name, &block)
  Rule.new(name, &block)
end

Note mais uma vez o uso de &amp; para passar o bloco adiante. Usando esse método, o bloco será passado três vezes como parâmetro até sua execução final.

Esse método deixa a regra assim:

r1 = rule("Rule 1") do

  group(:black, [:clubs, :spades])
  group(:red, [:diamonds, :hearts])

  group(:even) do |card| 
    card.value % 2 == 0
  end

  group(:odd) do |card| 
    card.value % 2 != 0
  end

  get(:black, :black)
  get(:odd)
  get(:red, :red, :red)
  get(:even)

  run("2 of clubs", "3 of spades", 
     "ace of clubs", "2 of hearts")
      
end

Esse é basicamente o resultado que queríamos. Os dois últimos ajustes farão o resto. Um ajuste no método group para usar um Hash pode deixá-lo mais livre:

class Rule
  
  def group(name, options = {})
    if options[:is].kind_of?(Array)
      @groups[name] = []
      options[:is].each do |group|
        @groups[name] += Card.all.select { |card|
          (card.suit == group) ||
          (card.face == group) 
        }
      end
    elsif block_given?
      @groups[name] = Card.all.select { |card| yield card }
    end    
  end

  # ...

end

E finalmente, podemos remover os parênteses desnecessários da regra, que fica assim:

r1 = rule "Rule 1" do

  group :black, :is => [:clubs, :spades]
  group :red, :is => [:diamonds, :hearts]

  group :even do |card| 
    card.value % 2 == 0
  end

  group :o dd do |card| 
    card.value % 2 != 0
  end

  get :black, :black
  get :o dd
  get :red, :red, :red
  get :even

end

r1.run("2 of clubs", "3 of spades", 
      "ace of clubs", "2 of hearts")

Finalmente temos o resultado que queríamos. Como é fácil perceber pelos passados dados, o grosso da transformação envolveu o uso de blocos para esconder as chamadas.

Basicamente qualquer DSL mais simples pode usar uma estratégia similar. De fato, a maioria das DSL encontradas como exemplo usam exatamente esse tipo de código, embora o mesmo tenha alguns problemas de integração com testes, especialmente no que tange ao uso de ferramentas como RSpec.

No próximo artigo veremos dois outros exemplos da aplicação de DSL usando algumas técnicas diferentes para criar possibilidades ainda mais interessantes.

Domain Specific Languages: Do problema à DSL

January 8th, 2008 § 6 comments § permalink

Este é o segundo artigo em uma série sobre DSL, ou Linguagens Específicas de Domínio. O primeiro artigo introduziu o assunto explicando o que é uma DSL, como elas podem melhorar e simplificar o código de uma aplicação e, finalmente, mostrando alguns exemplos usados pela maioria dos desenvolvedores.

Neste segundo artigo, explicaremos com um problema comum pode ser transformado em uma DSL para exemplificar o processo de pensamento por trás da criação de uma linguagem que se propõe a resolver um problema específico em uma aplicação.

Como veremos abaixo, uma DSL pode converter um problema espinhoso em algo relativamente trivial. Para dar uma idéia do impacto desse tipo de simplificação, uma das aplicações que eu desenvolvi recentemente envolve a análise de segurança de barragens. Como todo consultor de segurança diria, barragens tendem sempre a cair e o problema é saber quando e como evitar isso. Para descobrir esses dados, o programa agrega diversas informações e permite que o consultor extrapole previsões de risco para a instalação e tome as medidas apropriadas.

Uma das mais importantes informações adquiridas pelo programa são dados de instrumentação. Esses dados podem ser digitados no sistema ou coletados automaticamente em leitores apropriados. Para um dado cliente, o programa pode suportar se meia dúzia a milhares de instrumentos variante de dez a quarenta tipos diferentes. Em muitos casos, uma nova instalação do programa também evolve a criação de uma interface para um novo tipo de instrumento e tudo o que está associado ao mesmo: leituras, programação, projeções, gráficos, impressão e uma série de outros benefícios.

A estrutura criada pelo programa é relativamente flexível e permite a inserção de novos instrumentos de uma maneira comparativamente simples. O problema é integrar toda a lógica relacionada a um instrumento no resto do sistema. Da maneira como o programa está estruturado, isso é feito por classes específicas que lidam com cada tipo de instrumento. E, embora a integração final seja simples, testar cada um desses instrumentos é algo demorado.

A solução encontrada foi criar uma DSL para descrever especificações de instrumentos e criar toda a infra-estrutura necessária. Testando a DSL uma única vez seria a garantia de que todos instrumentos estariam automaticamente testados e não seria necessário nos preocuparmos com cada detalhe de implementação. Isso foi feito e o tempo de incorporação de um novo instrumento caiu de uma semana para duas horas. Os erros são mínimos e a aplicação tem funcionado bem ao contento de seus usuários.

Esse é o poder de uma DSL.

Um problema

Para mostrar essa variação de expressividade de uma forma bem prática, vamos imaginar um problema relativamente simples, também baseado em uma experiência recente minha. A solução que encontramos é parecida ao que Martin Fowler descreve em seu artigo sobre o mesmo tema de modo que toda similaridade não é coincidência.

Em uma das nossas aplicações, um dos problemas a serem resolvidos era a importação diária de várias planilhas que chegam em um centro de produção do cliente. Esses arquivos são gerados por aplicações de terceiros, aos quais não tínhamos acesso e variavam constantemente em seu formato. Alguns de mês para mês, outros de semana para semana. Parte do contrato, portanto, era criar uma forma simples de mapear esses arquivos de modo que mudanças pudessem ser tratados com eficiência. De fato, com um pouco de treinamento, mesmo os usuários da aplicação seriam capazes de elaborar novos mapeamentos e lidar com as variações mais comuns da importação.

Para facilitar, o exemplo será dado em Ruby, embora a linguagem original tenha sido o C# dentro de uma aplicação usando ASP.NET.

A maneira convencional de resolver um problema assim é criar um framework de processamento. Isso incluiria:

  • Uma classe para descrever formatos de leitura dos arquivos
  • Uma classe para descrever configurações de ambiente
  • Uma classe para ler arquivos que receberia esses formatos e devolveria registros processados
  • Uma classe para mapear os registros processados e passá-los a um serviço interno da aplicação
  • Uma classe para juntar todas acima em uma interface mais simples

Uma solução

Uma implementação simples, consistiria então de algo assim:

class Configuration
  def initialize(config_file); end
end

class Format
  def initialize(configuration); end
  def define_field(name, column, type, options); end
end

class Reader
  def initialize(configuration, filename, format); end
  def process; end
end

class Service; end
class MyService < Service; end

Essas classes seriam usadas da seguinte forma:

config = Configuration.new("config.yaml")

format = Format.new(config)
format.define_field("name", "a1", String)
format.define_field("date", "a2", Date, :year => Date.today.year)
format.define_field("order_id", "a3", Integer, :unique => true)

service = MyService.new

reader = Reader.new(config, "data.csv", format)

reader.process do |item|
  service.process(item.name, item.date, item.order_id)
  service.dispatch
end

Esse processo de resolução do problema é trivial e fácil de ser acompanhado. De fato, essa a estratégia que a maioria dos programadores escolhe ao resolver esse tipo de situação. A solução é flexível, pode ser entendida facilmente na presença de relativamente pouca documentação, e não oferece desafios à implementação.

A única coisa que a implementação acima não faz, e isso é o mais importante, é declarar o intento do processo que está sendo codificado. E aqui entra o maior valor de uma DSL.

Intento

O maior valor que uma DSL pode trazer a um problema é deixar claro o intento do mesmo na solução. Uma DSL, pela sua proximidade com uma linguagem de programação, possui a expressividade dessas últimas mas procura ser limitada naquilo que pode expressar justamente para conseguir reduzir a complexidade adjacente.

Se uma DSL é expandida com toda sorte de construções, ela deixa de ser uma DSL e se torna uma linguagem completa. Isso não impede é claro, que linguagens completas sejam implementadas com características de uma DSL. De fato, macros em Lisp ou Scheme e muito da gramática de Smalltalk são estruturadas para facilitar a criação de construções idiomáticas dentro de um programa que podem ser usadas com uma forma de DSL.

Em última instância, o objetivo de uma DSL, então, é fornecer intento ao código, ou seja, fazer com que o mesmo seja auto-documentado, que o próprio fato de ler o trecho em questão já revele o necessário para a compreensão mesmo. Uma DSL não é senão uma outra forma de abstrair a complexidade. Nesse sentido, elas sucedem historicamente a programação estrutura e a orientação a objetos, algo sobre o qual voltaremos a falar em breve.

Reduzindo o problema

Considerando a implementação mostrada, que, embora não seja muito complexa, não descreve apropriadamente o intento do código, como podemos convertê-la para uma DSL que faça isso? A solução é relativamente simples:

process_file :named => "data.csv", :using => MyService do
  map_column :a1, :to_field => :name, :formatted_as => String
  map_column :a2, :to_field => :date, :formatted_as => Date,
    :with_options => { :year => Date.today.year }
  map_column :a3, :to_field => :o rder_id, 
    :formatted_as => Integer,
    :with_options => { :unique => true }
end

Uma outra opção seria:

process_file("data.csv").using(MyService) do
  map_column(:a1).to_field(:name).formatted_as(String)
  map_column(:a2).to_field(:date).formatted_as(Date).
    with_options(:year => Date.today.year)
  map_column(:a3).to_field(:order_with).formatted_as(Integer).
    with_options(:unique => true)
end

Ambas são igualmente válidas no sentido que declaram o intento do código e podem ser lidas quase como um texto descrevendo o que estão executando.

É importante lembrar que uma DSL esconde a complexidade do código–ele não elimina a necessidade de escrever o código. Uma DSL bem projetado, entretanto, pode reduzir o código a ser escrito codificando instruções comuns em formatos mais acessíveis. Embora o caso mostrado acima seja um caso simples, a redução de código chega a 30% em termos brutos e com um ganho imenso em termos de legibilidade.

Em nosso próximo artigo, veremos a implementação de uma DSL em Ruby e como aproveitar os recursos que uma linguagem com meta-programação oferece para isso.

Domain Specific Languages: Introdução

January 7th, 2008 § 11 comments § permalink

Esse é o primeiro artigo de uma série sobre DSLDomain Specific Languages ou Linguagens Específicas de Domínio–que deve se estender por três ou quatro artigos.

O objetivo é explicar um pouco sobre o que é uma DSL e como você pode usá-las para facilitar o seu trabalho de programação, reduzindo e melhorando o seu código de maneira significativa. O conteúdo é baseado em palestras que eu dei anteriormente mas com algumas outras considerações que o tempo não permitiu durante as mesmas.

Linguagens

Um dos meus referenciais em relação ao aprendizado de programação é tentar seguir o que Alan Perlis disse sobre o assunto:

Se uma linguagem não é capaz de afetar o modo como você pensa sobre programação, não vale a pena aprendê-la.

Eu não sei se Perlis estava pensando em como linguagens podem afetar o modo como você programa, mas isso é algo que sempre me vêm à mente quando eu penso no que Perlis disse. Há uma cadeia lógica quando se pensa em linguagens, paradigmas e sintaxe. Paradigmas afetam a sintaxe da linguagem e a sintaxe afeta como você programa. Disso se segue que o desenho da linguagem necessariamente afeta como você programa. Isso parece óbvio mas o fato é que mesmo variações entre linguagens imperativas–o paradigma de maior sucesso na história da programação–podem ter um impacto significativo em com um programador é capaz de descrever os problemas em um dado domínio.

A extensão natural dessa progressão lógica é pensar o que poderia ser feito caso fosse possível implementar a sua própria linguagem. Um paradigma diferente ou uma sintaxe diferente poderiam modificar a maneira com um problema é tratado e simplificar enormemente sua resolução.

O que uma DSL se propõe a fazer é exatamente isso: tratar um problema específico, dentro de um domínio qualquer, através da criação de uma linguagem própria para o mesmo. E embora isso seja possível em maior ou menor grau em virtualmente qualquer linguagem de programação, existem certas características que facilitam esse tipo de programação. Um forte protocolo para objetos, uma sintaxe flexível e a capacidade de interceptar características da linguagem são essenciais para o aproveitamento máximo de uma DSL.

Nessa série de artigos usaremos Ruby para isso, embora os mesmos princípios possam ser aplicados em qualquer outra linguagem.

O que realmente é uma DSL?

O que hoje está se tornando conhecido como DSL atualmente já teve vários nomes dependendo da comunidade em que as mesmas eram usadas. Alguns desses nomes são:

  • Pequenas linguagens: comunidade Unix
  • Macros: usuários de Lisp e Scheme
  • Linguagens de aplicação: BI e analistas
  • Linguagens orientadas ao problema: designers de linguagens de programação

Não importa qual o nome, todos esses rótulos se referem ao mesmo conceito: linguagens (isto é, formas de expressar algum conceito) específicas (ou seja, com um propósito bem definido) de domínio (pertencentes a uma classe particular de problemas). Uma DSL é circunscrita por três definições:

  • Ela resolve um problema específico. Em outras palavras, uma DSL não tenta abarcar todos os problemas possíveis de uma aplicação, mas se fixas em um único deles. Pode existir cooperação entre várias linguagens para resolver um problema maior, mas uma DSL resolve um único problema.
  • Ela estreita o foco do domínio. Uma DSL permite que um domínio específico tenha o seu escopo reduzido através da abstração que a mesma providencia. Podem ser necessárias várias linguagens para esconder todo um problema, mas cada uma dela resolve uma parte e esconde a mesma atrás de uma abstração mais simples.
  • Ela esconde a complexidade do código. A DSL não pode ser mais complicada do que o código que pretende substituir. A abstração deve ser inequívoca mas com uma interface significativamente reduzida.

Com base em todos esses conceitos é fácil perceber que usamos várias dessas linguagens:

  • Um Makefile, por exemplo, usa uma DSL que permite especificar como determinados arquivos devem ser compilados/montados para formar uma aplicação.
  • Comandos para o shell de um sistema operacional são uma DSL para manipular os fluxos de entrada e saída de programas sendo executados. Um script é uma mera aplicação dessa linguagem.
  • SQL é uma linguagem específica para a definição e recuperação de dados descritos em um formato relacional. Por mais sofisticado que seja, ele se resume a quatro operações básicas: selecionar, inserir, atualizar e excluir.
  • Todo arquivo XML ou SGML é uma DSL em si própria. Assim, o HTML e o XHTML são aplicações específicas que também forma uma DSL por sua vez.

Um dos exemplos mais interessantes de uma DSL é a linguagem usada pelo Graphviz. Essa linguagem, chamada de DOT permite a criação de virtualmente qualquer tipo de grafo através de comandos simples e intuitivos. O código abaixo é um exemplo simples:

digraph finite_state_machine 
{
    rankdir = LR;
    size = "8,5"
    
    node [shape = doublecircle]; LR_0 LR_3 LR_4 LR_8;
    node [shape = circle];
    
    LR_0 -> LR_2 [ label = "SS(B)" ];
    LR_0 -> LR_1 [ label = "SS(S)" ];
    LR_1 -> LR_3 [ label = "S($end)" ];
    LR_2 -> LR_6 [ label = "SS(b)" ];
    LR_2 -> LR_5 [ label = "SS(a)" ];
    LR_2 -> LR_4 [ label = "S(A)" ];
    LR_5 -> LR_7 [ label = "S(b)" ];
    LR_5 -> LR_5 [ label = "S(a)" ];
    LR_6 -> LR_6 [ label = "S(b)" ];
    LR_6 -> LR_5 [ label = "S(a)" ];
    LR_7 -> LR_8 [ label = "S(b)" ];
    LR_7 -> LR_5 [ label = "S(a)" ];
    LR_8 -> LR_6 [ label = "S(b)" ];
    LR_8 -> LR_5 [ label = "S(a)" ];
}

O resultado é o seguinte:

É fácil observar pela galeria de exemplos desse pacote que o uso da DSL esconde uma complexidade enorme por trás das cenas mas é bem específica no problema que resolve.

Uma DSL é uma linguagem, mas ela não se propõe a ser completa. Ao contrário de linguagens de programação que possui construções específicas para lidar com problemas genéricos de condições, iterações e definições, uma DSL é um conjunto limitado de comandos que descrevem um intento específico sobre um problema específico.

Interna versus Externa

Uma DSL pode ser interna ou externa. Uma DSL externa geralmente pode ser utilizada pelos usuários finais da aplicação–e muitas vezes eles são o público real das mesmas–possuindo tratamento de erro mais específico para o domínio que resolvem. Em contrapartida, uma DSL internal normalmente é usada pelos próprios programadores da aplicação e não possui um tratamento de erros tão robusto porque assume que sua utilização será mais constrita.

O foco dos nossos artigos são as linguagens internas embora os princípios aqui discutidos também possam ser utilizados nas externas. O nosso objetivo é mostrar como a aplicação de linguagens internas pode reduzir o código necessário para uma aplicação e simplificar o processo de testes.

Em nosso próximo artigo veremos como um problema comum pode ser convertido de sua implementação normal para usar uma DSL que o torna mais compacto e inteligível.

Where Am I?

You are currently browsing the Ruby category at Superfície Reflexiva.