Pensamento Funcional: Repensando o Despacho

Como as linguagens JVM de próxima geração agregam nuance ao despacho do método

Linguagens de próxima geração para a plataforma Java™ possuem mecanismos de despacho de método mais flexíveis do que a linguagem Java. Nesta parte do artigo Pensamento funcional , Neal Ford explora os mecanismos de despacho em linguagens funcionais, como Scala e Clojure, mostrando novas formas de pensar sobre a execução de código.

Neal Ford, Software Architect / Meme Wrangler, ThoughtWorks Inc.

Neal FordNeal Ford é arquiteto de software e Meme Wrangler na ThoughWorks, uma consultoria global de TI. Ele também projeta e desenvolve aplicativos, materiais de instrução, matérias 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. Seu enfoque é o projeto e a criação de aplicativos corporativos de grande porte. Também é palestrante admirado em conferências de desenvolvedores no mundo todo. Conheça seu website website.



06/Set/2012

Sobre esta série

Esta série visa reorientar sua perspectiva para uma mentalidade funcional, ajudando a encarar problemas comuns de maneiras novas e encontrar maneiras de melhorar sua codificação diária. Ela explora os conceitos de programação funcional, estruturas que permitem programação funcional dentro da linguagem Java, linguagens de programação funcional executadas em JVM e algumas indicações de tendência futura do design da linguagem. A série é voltada para desenvolvedores que conhecem Java e como suas abstrações funcionam, mas têm pouca ou nenhuma experiência na utilização de uma linguagem funcional.

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 ifs 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 estruturas, 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

Aprender

Obter produtos e tecnologias

Discutir

Comentários

developerWorks: Conecte-se

Los campos obligatorios están marcados con un asterisco (*).


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


A primeira vez que você entrar no developerWorks, um perfil é criado para você. Informações no seu perfil (seu nome, país / região, e nome da empresa) é apresentado ao público e vai acompanhar qualquer conteúdo que você postar, a menos que você opte por esconder o nome da empresa. Você pode atualizar sua conta IBM a qualquer momento.

Todas as informações enviadas são seguras.

Elija su nombre para mostrar



Ao se conectar ao developerWorks pela primeira vez, é criado um perfil para você e é necessário selecionar um nome de exibição. O nome de exibição acompanhará o conteúdo que você postar no developerWorks.

Escolha um nome de exibição de 3 - 31 caracteres. Seu nome de exibição deve ser exclusivo na comunidade do developerWorks e não deve ser o seu endereço de email por motivo de privacidade.

Los campos obligatorios están marcados con un asterisco (*).

(Escolha um nome de exibição de 3 - 31 caracteres.)

Ao clicar em Enviar, você concorda com os termos e condições do developerWorks.

 


Todas as informações enviadas são seguras.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=833574
ArticleTitle=Pensamento Funcional: Repensando o Despacho
publish-date=09062012