DSL: Rolando dados com o Groovy
Provavelmente, muitos dos que acompanham este blog também acompanharam o evento Rails for Kids, que rolou no último sábado. Eu particularmente achei o evento muito bacana, principalmente agora que já terminei de ler o livro Agile Web Development with Rails e estou começando a levar o Rails mais a sério como opção de framework para criação de aplicações web. Mas não é sobre isso que falarei hoje.
Apesar de já ter lido um punhado de artigos e posts sobre DSLs, a palestra que o Ronaldo Ferraz ministrou nesse evento foi o incentivo de que precisava para tomar vergonha na cara e tentar implementar minha primeira DSL. Eu peguei um dos primeiros exemplos que ele citou em sua palestra — um rolador de dados — e tentei implementar para ver como ficava. O resultado dessa experiência será detalhado ao longo deste post.
Sobre a DSL
A idéia era criar uma DSL em Groovy que permitisse especificar rolagens de dados de RPG numa “linguagem” semelhante à forma que os próprios jogadores costumam usar. Claro que eu adicionei algumas firulas, mas os exemplos abaixo ilustram bem essa “linguagem”:
-
2.d10 + 5.d20
-
5.d - 2.d12
-
2.d20 - 5
-
4.d * 4
-
(5.d6 + 5) * 4
-
5.d6 + 5 * 4
Cada uma das linhas acima pode ser traduzida para as formas textuais correspondentes:
- Role 2 dados de 10 faces e 5 dados de 20 faces, e junte os resultados;
- Role 5 dados comuns (de 6 faces) e tire dos resultados todos os números sorteados ao rolar 2 dados de 12 faces;
- Role 2 dados de 20 faces e adicione um “-5″ nos resultados;
- Role 4 dados de 6 faces e multiplique cada número sorteado por “+4″;
- Role 5 dados de 6 faces, adicione um “+5″ nos resultados e multiplique cada número por “+4″;
- Role 5 dados de 6 faces e adicione nos resultados o retorno da multiplicação de 5 por 4.
E não parei por aí; além de permitir a simples rolagem de dados, a DSL também devia fornecer algumas formas de se obter informações sobre um determinado conjunto de resultados. Por exemplo:
-
10.d20.highest
-
5.d10.lowest.d6
-
(6.d12 - 2).sum
-
5.d20.average
-
(2.d10 + 6.d).best(3)
-
(2.d10 + 6.d).worst(3)
Novamente, cada uma das linhas acima podem ser traduzidas para:
- Role 10 dados de 20 faces e retorne o maior número que foi sorteado;
- Role 5 dados de 10 faces, pegue o menor número que foi sorteado e role dados de 6 faces por esse número de vezes;
- Role 6 dados de 12 faces, adicione um “-2″ e retorne a soma dos resultados;
- Role 5 dados de 20 faces e retorne a média dos números sorteados;
- Role 2 dados de 10 faces e 6 dados de 6 faces, e retorne os 3 melhores números sorteados;
- Role 2 dados de 10 faces e 6 dados de 6 faces, e retorne os 3 piores números sorteados.
Uma das características mais marcantes em DSLs bem projetadas é que tais linguagens podem ser facilmente entendidas por especialistas do domínio, mesmo se tais especialistas não tiverem nenhum conhecimento em programação.
Para ilustrar essa afirmação, podemos dizer sem medo de errar que uma instrução Groovy new DiceRollingSpec(sides:4).roll(5) pode não significar absolutamente nada para a maioria dos jogadores de RPG. Entretanto, se você já jogou RPG alguma vez na vida, sabe que uma instrução 5.d4 é facilmente entendida por qualquer jogador.
Preparado para o código? Então vamos nessa.
Primeiro: resolvendo o problema… a qualquer custo
Antes de enveredarmos na criação da DSL, precisamos criar código que nos forneça a funcionalidade necessária, que é rolar dados e colher informações sobre os mesmos.
Como o código é extremamente simples, não precisarei ficar explicando. Basta conhecer o básico da linguagem Groovy para entender. Veja só:
Com exceção dos três últimos métodos, o resto é extremamente simples de entender. Na verdade, os três últimos métodos sobrescrevem os operadores +, - e * para objetos DiceRollingSpec. Isso permite que objetos dessa classe sejam usados em alguns tipos de expressões como as citadas anteriormente. Um exemplo de expressão válida:
No entanto, esse código não possui a expressividade que gostaríamos de ver numa API de rolagem de dados.
Segundo: adicionando expressividade à solução
Como vimos, o ponto de partida para se especificar rolagens de dados se parece com o seguinte:
-
I.dX
I é um objeto Integer qualquer e indica o número de vezes que o dado deve ser lançado; dX indica o tipo de dado, que é codificado como sendo um método getter getDX(), onde X é o número de faces do dado.
Agora, qual a razão de eu ter escolhido usar um método getter a um método comum? Veja só o exemplo abaixo:
-
5.d12()
Infelizmente, o Groovy exige a digitação do “abre-e-fecha-parênteses” ao invocar métodos sem parâmetros. Como a idéia era representar o mais fielmente possível a forma com que jogadores de RPG especificam rolagens de dados, esse “abre-e-fecha-parênteses” acaba sujando o código e prejudicando sua expressividade; eu particularmente nunca vi alguém dizer “beleza, agora eu vou rolar cinco dê-doze abre-e-fecha-parênteses… esse monstro vai ver só uma coisa!”.
Mas beleza… agora que sabemos como deverá funcionar, precisamos arranjar um jeito de retornar objetos DiceRollingSpec sempre que getters getDX() forem invocados em objetos Integer. Felizmente, o Groovy fornece uma forma bem fácil de se fazer isso:
-
-
name = ‘d6′ // 6-sided dice is the default dice type
-
}
-
-
/* intercept calls to ‘dX’ properties */
-
.roll(value)
-
}
-
-
"No such property: $name for class: ${value.class}")
-
}
O Groovy fornece diversas soluções para modificar objetos e classes em runtime; a que parece mais adequada ao nosso problema é interceptar chamadas a getters getDX() e retornar objetos DiceRollingSpec correspondentes.
Outra solução — não muito boa — seria adicionar diversos métodos getters, um para cada tipo de dado. Embora tal solução seja bem mais “leve” que a solução escolhida, ela possui limitações no que diz respeito aos tipos de dados suportados; a primeira solução permite rolar qualquer tipo de dado enquanto que a segunda só suportaria determinados tipos de dados.
Falta alguma coisa?
Não! Basta executar o trecho de código mostrado anteriormente para que objetos Integer possam responder a chamadas getDX(). Como as operações +, - e * foram sobrescritas para objetos DiceRollingSpec, expressões como as mostradas no início deste post funcionam perfeitamente!
Agora que já conhecemos o código, vamos ver como funciona a execução de uma rolagem de dados qualquer, passo-a-passo:
-
(2.d10 + 6.d).best(3)
- O método
getD10()é chamado no objetoInteger 2. Um novo objetoDiceRollingSpec(sides:10)é criado e seu métodoroll()é invocado com o parâmetro2. Por exemplo, o resultado foi[8,3]. Tal objeto é retornado; - O método
getD()é chamado no objetoInteger 6. Um novo objetoDiceRollingSpec(sides:6)é criado e seu métodoroll()é invocado com o parâmetro6. Por exemplo, o resultado foi[5,1,3,6,5,2]. Tal objeto é retornado; - Os objetos retornados nos passos 1 e 2 são somados num novo objeto
DiceRollingSpec, resultando em[8,3,5,1,3,6,5,2]; - O método
best(3)é chamado no objetoDiceRollingSpeccriado no passo anterior, retornando o array[8,6,5].
Legal, não?! Fique tranqüilo se algum ponto ficou meio difícil de entender. O ideal seria ver a coisa rodando na sua frente e, por esse motivo, eu disponibilizei o código-fonte para download na página de aplicações aqui do blog. Sinta-se livre para baixar e brincar com o código, que, além de muitas linhas de comentários, possui um caso de testes que mostra detalhadamente o funcionamento da DSL.
Have fun!
UPDATE: Este post levou a criação de um projeto de software livre cujos detalhes podem ser conferidos aqui.
Tags: dados, dsl, evento, exemplo, groovy, java, meta programação, rails, rpg, ruby

17 de dezembro de 2007 às 2:06 pm
Excelente artigo! keep them coming!
17 de dezembro de 2007 às 2:26 pm
Obrigado pelo comentário, Andres! Fico feliz que tenha gostado.
17 de dezembro de 2007 às 2:46 pm
Good one! Keep groovin’!
17 de dezembro de 2007 às 3:19 pm
Thanks for your feedback, gnomiX!
17 de dezembro de 2007 às 6:34 pm
Excellent, I wish I knew how to read Portugese though
At least I really enjoyed the samples.
Congrats.
17 de dezembro de 2007 às 8:30 pm
Thanks, Guillaume! Needless to say that it wouldn’t be possible without the great work of all Groovy contributors.
Here in Brazil there are lots of programmers that don’t speak English at all. Since all the cool stuff today are written in English, it’s possible that these people face some difficulties to reach and benefit from it. The funny thing: if we look carefully, the very opposite is happening here!
I could translate this article to English, but as you might noticed, my English sucks. That’s why I don’t dare to write articles in English.
Fortunately, the sample code available is heavily tested and commented… in English!
See you.
17 de dezembro de 2007 às 8:53 pm
Excellent article.
I’ve written a dice rolling API in Java and would be very interested in adding support for complex expressions like you’ve demonstrated in your article.
I’ve tried using a math expression library, but a DSL may be a much better fit! If you’d be interested in contributing any code, feel free to contact me or checkout the shard project:
http://shard.dev.java.net
Dice API:
https://shard.dev.java.net/source/browse/shard/shard-dice/src/main/java/com/codecrate/shard/dice/
17 de dezembro de 2007 às 10:02 pm
Olá Daniel, estou usando o Groovy/Grails e estou gostando muito. O artigo está muito esclarecedor. Parabéns muito bom artigo!
17 de dezembro de 2007 às 10:04 pm
Hello, Ryan!
Did you get the sample application (available at my esnips profile)? Although the code shown here is incomplete, it shows everything you need to implement the behavior related in this article. I recommend you to download the sample application package… it contains better commented code and several test cases.
Once you understand the mechanics behind the code, you can easily invoke that code from within a Java application through the Java Scripting API.
Cheers,
Daniel.
17 de dezembro de 2007 às 10:08 pm
Olá, Regis! Finalmente um brasileiro aparecendo por aqui!
Eu também venho usando ambos os projetos e a cada dia que passa eu sinto menos vontade de voltar ao jeito “antigo”. Realmente… a diferença é gritante.
Valeu!