Pensamento Funcional
Repensando o Despacho
Como as linguagens JVM de próxima geração agregam nuance ao despacho do método
Conteúdos da série:
Esse conteúdo é a parte # de # na série: Pensamento Funcional
Esse conteúdo é parte da série:Pensamento Funcional
Fique ligado em conteúdos adicionais dessa série.
Na último artigo, eu explorei o uso de genéricos Java para representar o reconhecimento de padrões em Scala, o que permite escrever instruções condicionais concisas e legíveis. O reconhecimento de padrões Scala é um exemplo de mecanismo de despacho alternativo, que estou usando como um termo amplo para descrever as formas como as linguagens escolhem o comportamento dinamicamente. Esta parte do artigo estende a discussão para mostrar como os mecanismos de despacho em diversas linguagens de JVM funcionais permitem mais consciência e flexibilidade do que suas contrapartes Java.
Melhorando o despacho com o Groovy
No Java, a execução condicional termina usando a instrução if
, exceto em casos muito limitados em que a instrução switch
aplica-se. Como é muito difícil ler longas séries de instruções if
, os desenvolvedores Java dependem do padrão Factory (ou Factory Abstrato) do Gang of Four (GoF) (consulte Recursos). Caso use uma linguagem que inclua uma expressão de decisão mais flexível, é possível simplificar muito de seu código ainda mais.
Groovy tem uma instrução switch
eficiente que representa a sintaxe — mas não o comportamento — da instrução switch
do Java, conforme mostrado na Listagem 1:
Listagem 1. Instrução switch
amplamente melhorada de Groovy
class LetterGrade { def gradeFromScore(score) { switch (score) { case 90..100 : return "A" case 80..<90 : return "B" case 70..<80 : return "C" case 60..<70 : return "D" case 0..<60 : return "F" case ~"[ABCDFabcdf]" : return score.toUpperCase() default: throw new IllegalArgumentException("Invalid score: ${score}") } } }
A instrução switch
de Groovy aceita uma grande variedade de tipos dinâmicos. Na Listagem 1, o parâmetro score
deve ser um número entre 0 e 100 ou uma classificação por letra. Como no Java, você deve finalizar cada caso
com umretorno
ou quebra
, seguindo a mesma semântica de escape. Mas no Groovy, ao contrário do Java, eu posso especificar intervalos (90..100
, intervalos não inclusivos (80..<90
), expressões regulares (~"[ABCDFabcdf]"
) e uma condição padrão.
A digitação dinâmica do Groovy me permite enviar diferentes tipos de parâmetros e reagir apropriadamente, conforme mostrado nos testes de unidade na Listagem 2:
Listagem 2. Testando notas por letra do Groovy
@Test public void test_letter_grades() { def lg = new LetterGrade() assertEquals("A", lg.gradeFromScore(92)) assertEquals("B", lg.gradeFromScore(85)) assertEquals("D", lg.gradeFromScore(65)) assertEquals("F", lg.gradeFromScore("f")) }
Um switch
mais eficiente oferece um aterramento médio útil entre if
s seriais e o padrão de design Factory. O operador switch
permite que você corresponda intervalos e outros tipos complexos, o que tem intenção semelhante ao reconhecimento de padrões no Scala.
Reconhecimento de padrões Scala
O reconhecimento de padrões Scala permite que você especifique casos com um comportamento correspondente. Considere o exemplo de classificação por letra da parte do artigo anterior, mostrado na Listagem 3:
Listagem 3. Notas por letra no Scala
val VALID_GRADES = Set("A", "B", "C", "D", "F") def letterGrade(value: Any) : String = value match { case x:Int if (90 to 100).contains(x) => "A" case x:Int if (80 to 90).contains(x) => "B" case x:Int if (70 to 80).contains(x) => "C" case x:Int if (60 to 70).contains(x) => "D" case x:Int if (0 to 60).contains(x) => "F" case x:String if VALID_GRADES(x.toUpperCase) => x.toUpperCase }
No Scala, eu permito entrada dinâmica declarando o tipo de parâmetro como Qualquer
. O operador em ação é match
, que tenta corresponder a primeira condição verdadeira e retornar os resultados. Como mostra a Listagem 3, cada caso pode incluir uma condição de guarda que especifica condições.
A Listagem 4 mostra os resultados da execução de algumas opções de classificação por nota:
Listagem 4. Testando letras no Scala
printf("Amy scores %d and receives %s\n", 91, letterGrade(91)) printf("Bob scores %d and receives %s\n", 72, letterGrade(72)) printf("Sam never showed for class, scored %d, and received %s\n", 44, letterGrade(44)) printf("Roy transfered and already had %s, which translated as %s\n", "B", letterGrade("B"))
O reconhecimento de padrões no Scala geralmente é usado com as classes de caso do Scala, destinadas a representar tipos de dados algébricos e outros tipos de dados estruturados.
Linguagem "flexível" do Clojure
Outra linguagem funcional de próxima geração para a plataforma Java é o Clojure (consulte Recursos). Clojure — , uma implementação do Lisp na JVM, — tem uma sintaxe visivelmente diferente da maioria das linguagens contemporâneas. Embora os desenvolvedores se adaptem facilmente à sintaxe, ela dá a impressão de que os desenvolvedores Java atuais são estranhos. Um dos melhores recursos da família de linguagens é homoiconicidade, o que significa que a linguagem é implementada usando suas próprias estruturas de dados, permitindo extensão a um grau indisponível a outras linguagens.
Java e linguagens semelhantes incluem palavras-chave— a plataforma sintática da linguagem. Os desenvolvedores não podem criar novas palavras-chave na linguagem (embora algumas linguagens semelhantes ao Java permitam extensão via metaprogramação) e palavras-chave possuem semântica indisponível aos desenvolvedores. Por exemplo, a instrução if
Java "entende" coisas como a avaliação booleana de curto-circuito. Embora seja possível criar métodos e classes no Java, não é possível criar blocos de construção fundamentais, portanto é necessário converter problemas na sintaxe da linguagem de programação. (De fato, muitos desenvolvedores acham que seu trabalho é executar essa conversão.) Nas variantes do Lisp, como o Clojure, o desenvolvedor pode modificar a linguagem para o problema, confundindo a distinção entre qual linguagem o designer e os desenvolvedores usando a linguagem podem criar. Vou explorar todas as implicações da homoiconicidade em uma parte do artigo futura; a característica importante para entender aqui é a filosofia por trás do Clojure (e outros Lisps).
No Clojure, os desenvolvedores usam a linguagem para criar código legível (Lisp). Por exemplo, a Listagem 5 mostra o exemplo de nota por letra no Clojure:
Listagem 5. Notas por letra no Clojure
(defn letter-grade [score] (cond (in score 90 100) "A" (in score 80 90) "B" (in score 70 80) "C" (in score 60 70) "D" (in score 0 60) "F" (re-find #"[ABCDFabcdf]" score) (.toUpperCase score))) (defn in [score low high] (and (number? score) (<= low score high)))
Na Listagem 5, eu escrevi o método de nota por letra
para ler bem, depois implementei o método em
para que funcionasse. Neste código, a função cond
me permite avaliar uma sequência de testes, manipuladas por meu método in
. Como nos exemplos anteriores, eu manipulo as cadeias de caracteres numéricas existentes e as cadeias de caracteres de nota por letra. Finalmente, o valor de retorno deve ser um caractere maiúsculo, portanto, se a entrada estiver em minúscula, eu chamo o métodotoUpperCase
na cadeia de caracteres retornada. No Clojure, os métodos são cidadãos de primeira classe, e não classes, fazendo chamadas de método "inside-out": a chamada para score.toUpperCase()
no Java é equivalente no Clojure a (.toUpperCase score)
.
Testo a implementação de nota por letra do Clojure na Listagem 6:
Listagem 6. Testando notas por letra do Clojure
(ns nealford-test (:use clojure.test) (:use lettergrades)) (deftest numeric-letter-grades (dorun (map #(is (= "A" (letter-grade %))) (range 90 100))) (dorun (map #(is (= "B" (letter-grade %))) (range 80 89))) (dorun (map #(is (= "C" (letter-grade %))) (range 70 79))) (dorun (map #(is (= "D" (letter-grade %))) (range 60 69))) (dorun (map #(is (= "F" (letter-grade %))) (range 0 59)))) (deftest string-letter-grades (dorun (map #(is (= (.toUpperCase %) (letter-grade %))) ["A" "B" "C" "D" "F" "a" "b" "c" "d" "f"]))) (run-all-tests)
Nesse caso, o código de teste é mais complexo do que a implementação! No entanto, mostra como o código Clojure pode ser conciso.
No teste de notas com letra numérica
, eu desejo verificar cada valor nos intervalos apropriados. Se você não estiver familiarizado com o Lisp, a forma mais fácil de decodificá-lo é ler às avessas. Primeiro, o código #(é (= "A" (nota por letra %)))
cria uma nova função anônima que utiliza um único parâmetro (se você tiver uma função anônima que utilize um único parâmetro, é possível representá-lo como %
dentro do corpo) e retorna true
se a nota por letra correta for retornada. A função map
mapeia essa função anônima através da coleta no segundo parâmetro, que é a lista de números entre o intervalo apropriado. Em outras palavras, map
chama essa função em cada item na coleção, retornando uma coleção de valores modificados (que eu ignoro). A função dorun
permite que ocorram efeitos colaterais, dos quais depende a estrutura de teste. Chamar o mapa
através de cada intervalo
, na Listagem 6 retorna uma lista de todos os valores verdadeiros
. O método é
do namespace clojure.test
verifica o valor como um efeito colateral. Chamar a função de mapeamento em dorun
permite que o efeito colateral ocorra corretamente e retorna os resultados do teste.
Multimétodos Clojure
Uma longa série de instruções if
é difícil de ler e depurar, porém o Java não tem nenhuma alternativa especialmente válida no nível do idioma. Esse problema geralmente é resolvido usando os padrões de design Factory ou Factory Abstrato do GoF. O padrão Factory funciona no Java devido ao polimorfismo baseado em classe, que me permite definir uma assinatura de método geral na classe ou interface pai, depois escolher a implementação que executa dinamicamente.
Factories e polimorfismo
O Groovy tem uma sintaxe menos detalhada e mais fácil de ler do que o Java, portanto irei usá-lo no lugar do Java nos próximos exemplos de código— mas o polimorfismo funciona igual em ambos os idiomas. Considere essa combinação de interface e classes para definir um factory de Produto
, mostrado na Listagem 7:
Listagem 7. Criando um factory de produto no Groovy
interface Product { public int evaluate(int op1, int op2) } class Multiply implements Product { @Override int evaluate(int op1, int op2) { op1 * op2 } } class Incrementation implements Product { @Override int evaluate(int op1, int op2) { def sum = 0 op2.times { sum += op1 } sum } } class ProductFactory { static Product getProduct(int maxNumber) { if (maxNumber > 10000) return new Multiply() else return new Incrementation() } }
Na Listagem 7, eu crio uma interface para definir a semântica de com obter o produto de dois números e implementar duas versões diferentes do algoritmo. No ProductFactory
, eu determino as regras sobre qual implementação retorna do factory.
Uso o factory como um marcador abstrato para uma implementação concreta derivada por meio de alguns critérios de decisão. Por exemplo, considere o código na Listagem 8:
Listagem 8. Escolhendo dinamicamente uma implementação
@Test public void decisionTest() { def p = ProductFactory.getProduct(10010) assertTrue p.getClass() == Multiply.class assertEquals(2*10010, p.evaluate(2, 10010)) p = ProductFactory.getProduct(9000) assertTrue p.getClass() == Incrementation.class assertEquals(3*3000, p.evaluate(3, 3000)) }
Na Listagem 8, crio duas versões da minha implementação de Produto
, verificando se a correta retorna do factory.
No Java, a herança e o polimorfismo são conceitos fortemente acoplados: o polimorfismo desativa a classe do objeto. Em outras palavras, esse acoplamento é perdido.
Polimorfismo a la carte no Clojure
Muitos desenvolvedores recusam o Clojure, porque não é uma linguagem orientada a objetos, acreditando que linguagens orientadas a objetos estão no auge do poder. Isso é um erro: o Clojure tem todos os recursos de uma linguagem orientada a objetos, implementada independentemente de outros recursos. Por exemplo, ele suporta polimorfismo, mas não está restrito a avaliar a classe para determinar o despacho. O Clojure suporta múltiplos métodos polimórficos, de acordo com o qual o despacho é acionado independentemente da característica (ou combinação) que o desenvolvedor queira.
Aqui está um exemplo. No Clojure, os dados geralmente residem em estrutura
s, que representam a parte de dados de classes. Considere o código Clojure na Listagem 9:
Listando 9. Definindo uma estrutura de cores no Clojure
(defstruct color :red :green :blue) (defn red [v] (struct color v 0 0)) (defn green [v] (struct color 0 v 0)) (defn blue [v] (struct color 0 0 v))
Na Listagem 9, eu defino uma estrutura que possui três valores, correspondendo aos valores de cor. Também crio três métodos que retornam uma estrutura saturada com uma única cor.
Um multimétodo no Clojure é uma definição de método que aceita uma função de despacho, que retorna os critérios de decisão. Definições subsequentes permitem detalhar diferentes versões do método.
A Listagem 10 mostra um exemplo de uma definição multimétodo:
Listagem 10. Definindo um multimétodo
(defn basic-colors-in [color] (for [[k v] color :when (not= v 0)] k)) (defmulti color-string basic-colors-in) (defmethod color-string [:red] [color] (str "Red: " (:red color))) (defmethod color-string [:green] [color] (str "Green: " (:green color))) (defmethod color-string [:blue] [color] (str "Blue: " (:blue color))) (defmethod color-string :default [color] (str "Red:" (:red color) ", Green: " (:green color) ", Blue: " (:blue color)))
Na Listagem 10, defino uma função de despacho chamada cores básicas em
, que retorna um vetor de todos os valores de cor diferentes de zero. Para as variações no método, especifico o que deve acontecer se a função de despacho retornar uma única cor; nesse caso, retorna uma cadeia de caractere com sua cor. O último caso inclui a palavra-chave :default
, que manipula os casos restantes. Para esse, não posso presumir que recebi apenas uma cor, portanto o retorno lista os valores de todas as cores.
Testes para utilizar esses multimétodos aparecem na Listagem 11:
Listagem 11. Testando cores no Clojure
(ns colors-test (:use clojure.test) (:use colors)) (deftest pure-colors (is (= "Red: 5" (color-string (struct color 5 0 0)))) (is (= "Green: 12" (color-string (struct color 0 12 0)))) (is (= "Blue: 40" (color-string (struct color 0 0 40))))) (deftest varied-colors (is (= "Red:5, Green: 40, Blue: 6" (color-string (struct color 5 40 6)))))
Na Listagem 11, quando eu chamo o método com uma única cor, ele executa a versão colorida singular do multimétodo. Se eu o chamo com um perfil colorido complexo, o método padrão retorna todas as cores.
O desacoplamento do polimorfismo de herança fornece um mecanismo de despacho eficiente e contextualizado. Por exemplo, considere o problema de formatos de arquivo de imagem, cada um tendo um conjunto diferente de características para definir o tipo. Usando uma função de despacho, o Clojure permite construir despacho eficiente como contextualizado como polimorfismo Java, mas com menos limitações.
Conclusão
Nesta parte do artigo, forneci um tour rápido de vários mecanismos de despacho que vêm em linguagens JVM de próxima geração. Trabalhar em uma linguagem com despacho limitado geralmente aglomerar seu código com soluções alternativas extras como padrões de design. Escolher alternativas de novas linguagens onde nenhuma existia pode ser difícil, porque é necessário deslocar paradigmas; faz parte do aprendizado pensar em funcionalidade.
Recursos para download
Temas relacionados
- 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.
- Scala: é uma linguagem moderna e funcional em JVM.
- Clojure: Clojure é um Lisp moderno e funcional que é executado na JVM.
- Functional Java: é uma estrutura que acrescenta muitos desenvolvimentos de linguagem funcional a Java.
- 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.
- "Execution in the Kingdom of Nouns" (Steve Yegge, Março de 2006): An entertaining rant about some aspects of Java language design.
- Faça o download das versões de avaliação de produto IBM ou explore as versões de teste online 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®.