Nesse artigo sobre pensamento funcional, continuo minha investigação de soluções funcionais alternativas para os padrões de design da Gang of Four (GoF) (consulte Recursos). Neste artigo, investigo o menos compreendido, mas o mais eficiente desses padrões: o Interpretador.
A definição de Interpretador é:
Dada uma linguagem, defina uma representação para sua gramática juntamente com um interpretador que usa a representação para interpretar sentenças na linguagem.
Em outras palavras, se a linguagem sendo utilizada não for apropriada para o problema, use-a para desenvolver uma linguagem que seja. Bons exemplos dessa abordagem aparecem nas estruturas da web, como Grails e Ruby on Rails (consulte Recursos), que estendem seus idiomas de base (Groovy e Ruby, respectivamente) para facilitar a escrita de aplicativos da web.
Esse padrão é o menos compreendido , porque não é comum desenvolver uma nova linguagem, então, as qualificações e idiomas solicitados são especializados. É o mais eficiente dos padrões de desenvolvimento, porque incentiva a extensão da sua linguagem de programação para o problema que está sendo resolvido. Esse é um ethos comum nos mundos Lisp (e, portanto, Clojure), mas menos comum em linguagens mainstream.
Ao usar linguagens (como Java) que não permitem extensões para a própria linguagem, os desenvolvedores tendem a pensar na sintaxe da linguagem; é a única opção disponível. No entanto, ao se acostumar a trabalhar em linguagens que permitem a extensão sem problemas, você começa a direcionar a linguagem para a solução do problema, não o contrário.
Java não possui mecanismos simples de extensão de linguagem, a menos que recorramos à programação orientada a aspectos. No entanto, as linguagens JVM de próxima geração (Groovy, Scala e Clojure) (consulte Recursos) permitem diversos tipos de extensão. Ao fazer isso, elas satisfazem o objetivo do padrão de design do Interpretador. Primeiro, mostrarei como implementar a sobrecarga do operador em todas as três linguagens; em seguida, mostrarei como Groovy e Scala permitem estender classes existentes.
Sobrecarga do operador
Uma característica comum das linguagens funcionais é a sobrecarga do operador – , a capacidade de redefinir operadores (como +, -ou *) para trabalhar com novos tipos e demonstrar novos comportamentos. A omissão de sobrecarga do operador foi uma decisão consciente durante o período de formação do Java, mas praticamente todas as linguagens modernas a apresentam agora, incluindo os sucessores naturais do Java na JVM.
Groovy
O Groovy tenta atualizar a sintaxe do Java para o século atual, enquanto preserva sua semântica natural. Assim, o Groovy permite a sobrecarga do operador mapeando automaticamente os operadores para nomes de métodos. Por exemplo, se desejar sobrecarregar o operador Integer+ , substitua Integer , plus() substituído. A lista completa de mapeamentos está disponível online (consulte Recursos); a Tabela 1 mostra uma lista parcial:
| Operador | Método |
|---|---|
| x + y | x.plus(y) |
| x * y | x.multiply(y) |
| x / y | x.div(y) |
| x ** y | x.power(y) |
Como exemplo de sobrecarga do operador, criarei uma classe ComplexNumber no Groovy e no Scala. Números complexos são um conceito matemático com uma parte real e imaginária , geralmente escrito como, por exemplo, 3 + 4i. Os números complexos são comuns em muitos campos científicos, incluindo engenharia, física, eletromagnetismo e teoria do caos. Os desenvolvedores que escrevem aplicativos nesses campos se beneficiam imensamente com a capacidade de criar operadores que espelham seu domínio do problema. (Para obter mais informações sobre números complexos, consulte Recursos).
Uma classe Groovy ComplexNumber aparece na Listagem 1:
package complexnums
class ComplexNumber {
def real, imaginary
public ComplexNumber(real, imaginary) {
this.real = real
this.imaginary = imaginary
}
def plus(rhs) {
new ComplexNumber(this.real + rhs.real, this.imaginary + rhs.imaginary)
}
def multiply(rhs) {
new ComplexNumber(
real * rhs.real - imaginary * rhs.imaginary,
real * rhs.imaginary + imaginary * rhs.real)
}
String toString() {
real.toString() + ((imaginary < 0 ? "" : "+") + imaginary + "i").toString()
}
}
Na Listagem 1, crio uma classe que detém partes reais e imaginárias e crio os seguintes operadores sobrecarregados: plus() e um multiply() . É simples incluir dois números complexos: o operador plus() inclui as respectivas partes reais e imaginárias dos dois números umas às outras para produzir o resultado. A multiplicação de dois números complexos requer esta fórmula:
(x + yi)(u + vi) = (xu - yv) + (xv + yu)i
O valor multiply() na Listagem 1 replica a fórmula. Ele multiplica partes reais dos números, em seguida, subtrai o produto das partes imaginárias, que é incluído ao produto das partes reais e imaginárias multiplicadas uma pela outra.
A Listagem 2 testa os operadores de número complexo:
package complexnums
import org.junit.Test
import static org.junit.Assert.assertTrue
import org.junit.Before
class ComplexNumberTest {
def x, y
@Before void setup() {
x = new ComplexNumber(3, 2)
y = new ComplexNumber(1, 4)
}
@Test void plus_test() {
def z = x + y;
assertTrue 3 + 1 == z.real
assertTrue 2 + 4 == z.imaginary
}
@Test void multiply_test() {
def z = x * y
assertTrue(-5 == z.real)
assertTrue 14 == z.imaginary
}
}
Na Listagem 2, o uso do método plus_test() e um multiply_test() dos operadores sobrecarregados, – ambos representados pelos mesmos símbolos que os especialistas no domínio utilizam, – é indistinguível do uso semelhante de tipos integrados.
Scala (e Clojure)
Scala permite a sobrecarga do operador, descartando a distinção entre os operadores e os métodos: os operadores são apenas métodos com nomes especiais. Desse modo, para substituir o operador de multiplicação no Scala, você substitui o * método. Crio números complexos no Scala na Listagem 3:
class ComplexNumber(val real:Int, val imaginary:Int) {
def +(operand:ComplexNumber):ComplexNumber = {
new ComplexNumber(real + operand.real, imaginary + operand.imaginary)
}
def *(operand:ComplexNumber):ComplexNumber = {
new ComplexNumber(real * operand.real - imaginary * operand.imaginary,
real * operand.imaginary + imaginary * operand.real)
}
override def toString() = {
real + (if (imaginary < 0) "" else "+") + imaginary + "i"
}
}
A classe na Listagem 3 inclui os membros real e imaginário familiares, bem como o + e operadores/métodos de * . Como podemos ver na Listagem 4, posso usar o ComplexNumbers normalmente:
val c1 = new ComplexNumber(3, 2)
val c2 = new ComplexNumber(1, 4)
val c3 = c1 + c2
assert(c3.real == 4)
assert(c3.imaginary == 6)
val res = c1 + c2 * c3
printf("(%s) + (%s) * (%s) = %s\n", c1, c2, c3, res)
assert(res.real == -17)
assert(res.imagO operador Expando e classes de categoriaeradores e métodos, o Scala torna a sobrecarga do operador algo trivial. O Clojure usa o mesmo mecanismo para sobrecarregar operadores. Por exemplo, este código do Clojure define um operador ** sobrecarregado:
(defn ** [x y] (Math/pow x y))
Estendendo classes
Da mesma forma que a sobrecarga do operador, as linguagens JVM de próxima geração permitem estender as classes (incluindo as principais classes Java) de maneiras que são impossíveis na linguagem Java em si. Essas instalações são usadas com frequência para desenvolver linguagens de domínio específico (DSLs). Embora a GoF nunca tenha considerado as DSLs – porque elas não eram comuns nas linguagens populares da época – elas exemplificam o propósito original do padrão de design do Interpretador.
Ao incluir unidades e outros modificadores de classes principais, como Integer, é possível – como na inclusão de operadores – modelar problemas reais de forma mais próxima. Groovy e Scala permitem isso, mas com mecanismos diferentes.
O operador Expando e classes de categoria
O Groovy traz dois mecanismos para incluir métodos a classes existentes: ExpandoMetaClass e um categorias. (Abordei detalhes do ExpandoMetaClass no último artigo, no contexto do padrão do Adaptador).
Digamos que sua empresa, por motivos bizarros de legado, precise expressar velocidades em furlongs por quinzena, em vez de milhas por hora (MPH) e os desenvolvedores precisem realizar essa conversão muitas vezes. Usando o ExpandoMetaClassdo Groovy, é possível incluir uma propriedade FF ao Integer que manipula a conversão, conforme mostrado na Listagem 5:
static {
Integer.metaClass.getFF { ->
delegate * 2688
}
}
@Test void test_conversion_with_expando() {
assertTrue 1.FF == 2688
}
A alternativa ao ExpandoMetaClass é criar uma classe do wrapper de categoria , um conceito emprestado do Objective-C. Na Listagem 6, incluo uma (minúscula) propriedade ff ao Integer:
class FFCategory {
static Integer getFf(Integer self) {
self * 2688
}
}
@Test void test_conversion_with_category() {
use(FFCategory) {
assertTrue 1.ff == 2688
}
}
Uma classe de categoria é uma classe regular com uma coleção de métodos estáticos. Cada método aceita, pelo menos, um parâmetro; o primeiro parâmetro é o tipo que esse método aumenta. Por exemplo, no Listagem 6, a classe FFCategory possui um método getFf() , que aceita um Integer . Quando essa classe de categoria é usada com a palavra-chave usam , todos os tipos apropriados do bloco de códigos são aumentados. No teste de unidade, posso fazer referência à propriedade ff (lembre-se de que o Groovy converte automaticamente métodos get em referências de propriedades) no bloco de códigos, conforme mostrado na parte inferior da Listagem 6.
Ter dois mecanismos para escolher permite controlar o escopo de aumentos com mais exatidão. Por exemplo, se todo o sistema usa MPH como a unidade padrão de velocidade, mas também requer a conversão frequente para furlongs por quinzena, uma mudança mundial usando o ExpandoMetaClass seria adequada.
Podemos ser céticos com relação à utilidade das principais classes JVM de reabertura, nos preocupando com as amplas implicações. As classes de categoria permitem a limitação do escopo de aprimoramentos potencialmente perigosos. Segue um exemplo de um projeto real de software livre que faz excelente uso desse mecanismo.
O projeto easyb (consulte Recursos) permite escrever testes que verificam aspectos de classes em teste. Considere o fragmento de um teste easyb mostrado na Listagem 7:
ele "deve desenfileirar itens na mesma ordem em que foram enfileirados", {
[1..5].each {val ->
queue.enqueue(val)
}
[1..5].each {val ->
queue.dequeue().shouldBe(val)
}
}
A classe queue não inclui um método shouldBe(), que pode ser chamado durante a fase de verificação do teste. A estrutura easyb incluiu o método para mim; a definição do método it()na origem do easyb, mostrada na Listagem 8, mostra como:
def it(spec, closure) {
stepStack.startStep(BehaviorStepType.IT, spec)
closure.delegate = new EnsuringDelegate()
try {
if (beforeIt != null) {
beforeIt()
}
listener.gotResult(new Result(Result.SUCCEEDED))
use(categories) {
closure()
}
if (afterIt != null) {
afterIt()
}
} catch (Throwable ex) {
listener.gotResult(new Result(ex))
} finally {
stepStack.stopStep()
}
}
class BehaviorCategory {
// ...
static void shouldBe(Object self, value) {
shouldBe(self, value, null)
}
//...
}
Na Listagem 8, o método it() aceita uma spec (uma sequência que descreve o teste) e um bloco de encerramento que representa o corpo do teste. No meio do método, o encerramento é executado no bloco BehaviorCategory , que aparece na parte inferior da listagem. O valor BehaviorCategory aumenta o Object, permitindo que qualquer instância no universo Java verifique o seu valor.
Ao permitir o aumento seletivo de Object, que reside no topo da hierarquia, o mecanismo de classe aberta do Groovy permite verificar os resultados facilmente para qualquer instância, mas limita a mudança para o corpo do usam .
Casts implícitas do Scala
O Scala usa casts implícitas para simular o aumento de classes existentes. As casts implícitas não incluem métodos às classes, mas permitem que a linguagem converta automaticamente um objeto para um tipo apropriado que possua o método desejado. Por exemplo, não posso incluir um método isBlank() à classe String , mas posso criar uma conversão implícita que automaticamente converte Strings para uma classe que possua esse método.
Por exemplo, desejo incluir um método append() para Array, o que permite incluir instânciasPerson facilmente a uma matriz de tipo adequado, conforme mostrado na Listagem 9:
classe de caso Person (firstName: String, lastName: String) {}
classe PersonWrapper(a: Array[Person]) {
def append(other: Person) = {
a ++ Array(other)
}
def +(other: Person) = {
a ++ Array(other)
}
}
implicit def listWrapper(a: Array[Person]) = new PersonWrapper(a)
Na Listagem 9, crio uma classe Person simples com algumas propriedades. Para tornar Array[Person] (no Scala, uso genérico [ ] , em vez de < > como delimitadores) Person ciente, crio uma classe PersonWrapper , que inclui o método append() desejado. Na parte inferior da listagem, crio a conversão implícita que automaticamente converterá um Array[Person] a um PersonWrapper quando o método append() for chamado na matriz. A Listagem 10 testa a conversão:
val p1 = new Person("John", "Doe")
var people = Array[Person]()
people = people.append(p1)
Na Listagem 9, também incluo um método + à classe PersonWrapper . A Listagem 11 mostra como essa versão bem intuitiva do operador é usada:
people = people + new Person("Fred", "Smith")
for (p <- people)
printf("%s, %s\n", p.lastName, p.firstName)
Na verdade, o Scala não está incluindo um método à classe original, mas ele parece fazer isso, convertendo automaticamente para um tipo adequado. A mesma diligência necessária para a metaprogramação em linguagens como Groovy é necessária no Scala para evitar a criação de teias complicadas de classes interconectadas usando muitas casts implícitas. No entanto, quando usadas corretamente, as casts implícitas ajudam na escrita de códigos bastante expressivos.
Conclusão
O padrão de design original do Interpretador a partir da GoF sugeriu a criação de uma nova linguagem, mas suas linguagens de base não oferecem suporte aos mecanismos de extensão atrativos que temos à nossa disposição atualmente. Todas as linguagens Java de próxima geração suportam extensibilidade no nível da linguagem, usando diversas técnicas. Nesta edição, demonstrei como a sobrecarga do operador funciona no Groovy, Scala e Clojure e investiguei a extensão de classe no Groovy e Scala.
Em uma edição futura, mostrarei como uma combinação de correspondência de padrão estilo Scala e genérico permite a substituição de vários padrões de projetos tradicionais. O essencial para essa discussão é um conceito que também desempenha um papel na manipulação de erro de estilo funcional, que é o tema da próxima edição.
***
O IBM Forms fornece uma solução de formulários eletrônicos com área de cobertura zero para ajudá-lo a automatizar e retirar processos de negócio com base em formulários do desktop e movê-los para a Web, onde se integram com suas operações gerais de negócios. Ele fornece uma experiência do usuário atraente e interativa, custo total de propriedade menor e automação de processos aprimorada.
Recursos
Aprender
- The Productive Programmer (Neal Ford, O’Reilly Media, 2008): O livro mais recente de Neal Ford trata de ferramentas e práticas que ajudam a melhorar sua eficiência em codificação.
- Design Patterns: Elements of Reusable Object-Oriented Software (Erich Gamma et al., Addison-Wesley, 1994): o trabalho clássico da Gang of Four sobre padrões de design.
- Número complexo: Números complexos são uma abstração matemática que é aplicada em muitos campos científicos.
- Scala: é uma linguagem moderna e funcional em JVM.
- Clojure: Clojure é um Lisp moderno e funcional que é executado na JVM.
- Groovy: Groovy é uma linguagem moderna e dinâmica na JVM com muitos aspectos funcionais.
- Sobrecarga do operador no Groovy: Esta página mostra a lista completa de operadores suportados no Groovy e os métodos para os quais são mapeados.
- “Practically Groovy: Metaprogramming with closures, ExpandoMetaClass, and categories” (Scott Davis, developerWorks, junho de 2009): Saiba mais sobre metaprogramação no Groovy.
- easyb: easyb é uma ferramenta de desenvolvimento orientada ao comportamento de software livre desenvolvida no Groovy para ser usada com projetos Groovy e Java.
- “Drive development with easyb” (Andrew Glover, developerWorks, novembro de 2008): Descubra como o easyb ajuda na colaboração de desenvolvedores e partes interessadas.
- Grails: Grails é uma estrutura da web de software livre escrita em Java e Groovy.
- Ruby on Rails: Rails é uma estrutura da web de software livre escrita em Ruby, que é executada no JRuby.
- Procure na livraria de tecnologia para ver livros sobre este e outros tópicos técnicos.
- Zona tecnologia Java do developerWorks: Encontre centenas de artigos sobre quase todos os aspectos da programação Java.
Obter produtos e tecnologias
- Faça o download das versões de avaliação de produto IBM ou explore as versões de teste on-line no IBM SOA Sandbox e tenha contato com as ferramentas de desenvolvimento de aplicativos e produtos de middleware do DB2®, Lotus®, Rational®, Tivoli®e WebSphere®.
Discutir
Confira blogs do developerWorks e participe da comunidade do developerWorks.
***
Sobre o autor: Neal Ford é arquiteto de software e Meme Wrangler na ThoughtWorks, uma consultoria global de TI. Ele também projeta e desenvolve aplicativos, materiais de instrução, artigos para revistas, treinamentos e apresentações em vídeo/DVD e é autor ou editor de livros que abordam várias tecnologias, incluindo o mais recente The Productive Programmer. Sua especialidade é o projeto e a criação de aplicativos corporativos de grande porte. Também é orador internacionalmente aclamado nas conferências de desenvolvedores ao redor do mundo. Conheça seu website website.
***
Artigo original disponível em: http://www.ibm.com/developerworks/br/library/j-ft12/index.html
Qual a sua opinião?