Avançar para a área de conteúdo

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

Na primeira vez que você efetua sign in no developerWorks, um perfil é criado para você. Informações selecionadas do seu perfil developerWorks são exibidas ao público, mas você pode editá-las a qualquer momento. Seu primeiro nome, sobrenome (a menos que escolha ocultá-los), e seu nome de exibição acompanharão o conteúdo que postar.

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

  • Fechar [x]

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.

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

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

  • Fechar [x]

Resolvendo o problema da expressão com Clojure 1.2

Estenda tipos preexistentes para novos métodos, e métodos preexistentes para novos tipos

Stuart Sierra, Developer, Relevance, Inc.
Stuart Sierra
Stuart Sierra é ator/escritor/codificador que vive em na cidade de Nova York. Ele é membro da equipe Clojure/core em Relevance, Inc. Stuart é coautor de Practical Clojure (Apress, 2010). Ele é mestre em ciência da computação pela Columbia University e bacharel em teatro pela New York University.

Resumo:  O especialista em Clojure Stuart Sierra apresenta os novos recursos no Clojure 1.2 que resolvem o Problema da Expressão, um clássico dilema de programação. Protocolos permitem estender tipos preexistentes para novos métodos e tipos de dados permite estender métodos preexistentes para novos tipos — tudo sem alterar o código existente. Também será mostrado como interfaces e classes Java ™ podem interagir com protocolos e tipos de dados Clojure.

Data:  14/Dez/2010
Nível:  Intermediário Também disponível em :   Inglês
Atividade:  1584 visualizações
Comentários:  


Protocolos são um novo recurso apresentado na versão 1.2 do Clojure, uma linguagem de programação dinâmica para a JVM. Protocolos representam uma abordagem para programação orientada a objetos mais flexível que as hierarquias de classe Java, sem sacrificar o excelente desempenho do despacho do método JVM. Protocolos — e um recurso relacionado tipos de dados — fornecem uma solução para o que é conhecido como o Problema da Expressão. Resolver o Problema da Expressão torna possível estender os tipos preexistentes para novos métodos e estender métodos preexistentes para novos tipos, tudo sem compilar novamente o código existente.

Fundamentos do Clojure

Este artigo presume que você esteja familiarizado com os fundamentos de escrever e executar os programas Clojure (e Java). Para uma introdução ao Clojure, consulte o artigo do developerWorks "A linguagem de programação Clojure" e explore os Recursos .

Este artigo descreve o Problema da Expressão, mostra alguns exemplos dele e demonstra como os protocolos e tipos de dados do Clojure podem resolvê-lo, o que simplifica certos desafios de programação. Também será apresentado como é possível integrar os recursos de tipo de dados e o protocolo do Clojure com classes e interfaces Java existentes.

Muitos tipos, uma interface

Um dos principais recursos do Clojure é sua API de manipulação de dados genérica. Um pequeno conjunto de funções pode ser usado em todos os tipos integrados do Clojure. Por exemplo: a função conj (abreviatura de conjoin) adiciona um elemento a qualquer coleção, como mostrado na seção REPL a seguir:

user> (conj [1 2 3] 4)
[1 2 3 4]
user> (conj (list 1 2 3) 4)
(4 1 2 3)
user> (conj {:a 1, :b 2} [:c 3])
{:c 3, :a 1, :b 2}
user> (conj #{1 2 3} 4)
#{1 2 3 4}

Cada estrutura de dados comporta-se de maneira ligeiramente diferente em resposta à função conj (as listas crescem na frente, os vetores crescem no final, e assim por diante), mas todas dão suporte à mesma API. Este é um exemplo clássico de polimorfismo — muitos tipos acessados a partir de uma interface uniforme.

Polimorfismo é um potente recurso e uma das bases das linguagens de programação modernas. A linguagem Java dá suporte a um tipo particular de polimorfismo chamado polimorfismo de subtipo, que significa que uma instância de um tipo (classe) pode ser acessada como se fosse uma instância de outro tipo.

Em termos práticos, isso significa que é possível trabalhar com objetos através de uma interface genérica como java.util.List sem saber ou se importar se um objeto é ArrayList, LinkedList, Stack, Vector, ou algo deferente. A interface java.util.List define um contrato que qualquer classe que alegue implementar java.util.List deve cumprir.


O Problema da Expressão

Philip Wadler, do Bell Labs, cunhou o termo Problema da Expressão em um artigo não publicado que circulou por e-mail em 1998 (consulte Recursos ). Como ele disse, "O Problema da Expressão" é um novo nome para um problema antigo. A meta é definir um tipo de dados por casos, em que se pode adicionar novos casos ao tipo de dados e novas funções sobre o tipo de dados, sem recompilar o código existente e ao mesmo tempo em que se mantém a segurança de tipo estático (p. ex., sem casts)."

Para ilustrar o Problema da Expressão, o artigo de Wadler usa a ideia de uma tabela que possui tipos como linhas e funções como colunas. Linguagens orientadas a objeto tornam fácil adicionar novas linhas — ou seja, novos tipos estendendo uma interface conhecida — como mostrado na Figura 1:


Figura 1. Linguagens orientadas a objetos: fácil de adicionar novas linhas (tipos)
Image of a table in which each column represents a method in the java.util.List interface: List.add, List.get, List.clear, and List.size. Each of the first four row represents a class that implements java.util.List: ArrayList, LinkedList, Stack, and Vector. The cells where these rows and columns intersect represent existing implementations of the methods for each of the classes. A fifth row added at the bottom represents a new class you might write that implements java.util.List. For each cell in that row, you would write your own implementation of the corresponding method in java.util.List, specific to your new class.

Na Figura 1, cada coluna representa um método em java.util.List . Para simplicidade, inclui apenas quatro métodos: List.add, List.get, List.cleareList.size. Cada uma das primeiras quatro linhas representa uma classe que implementa java.util.List : ArrayList, LinkedList, StackeVector. As células onde essas linhas e colunas fazem interseção representam implementações existentes (fornecidas pela biblioteca da Classe Java padrão) dos métodos para cada uma das classes. Uma quinta linha adicionada à parte inferior representa uma nova classe que você pode escrever que implementa java.util.List. Para cada célula nessa linha, você escreveria sua própria implementação do método correspondente em java.util.List, específico para sua nova classe.

Em contraste a linguagens orientadas a objeto, linguagens funcionais normalmente tornam fácil adicionar novas colunas — ou seja, novas funções que opera sobre tipos existentes. Para o Clojure, isso pode ser parecido com a Figura 2:


Figura 2. Linguagens funcionais: fácil de adicionar novas colunas (funções)
Image of a table in which the columns represent functions in Clojure's standard collection API: conj, nth, empty, and count. The rows represent Clojure's built-in collection types: list, vector, map, and set. The cells where these rows and columns intersect represent existing implementations of these functions provided by Clojure. You can add a new column to the table by defining a new function. Assuming your new function is written in terms of Clojure's built-in functions, it will automatically be able to support all the same types.

A Figura 2 é outra tabela, similar à Figura 1. Aqui, as colunas representam funções na API da coleção padrão do Clojure: conj, nth, emptyecount. Similarmente, as linhas representam os tipos de coleção integrados do Clojure: list, vector, mapeset. As células onde linhas e colunas fazem interseção representam implementações existentes dessas funções fornecidas pelo Clojure. É possível adicionar uma nova coluna à tabela definindo uma nova função. Presumindo que sua nova função seja escrita em termos de funções integradas do Clojure, ela automaticamente poderá dar suporte a todos os mesmos tipos.

O artigo de Wadler estava falando sobre linguagens com tipos estáticos, como a linguagem Java. O Clojure tem tipos dinâmicos — os tipos específicos de objetos não precisam ser declarados, ou conhecidos no momento da compilação. Mas isso não significa que o Clojure tenha uma passagem livre para o Problema da Expressão. Na verdade, não muda praticamente nada. Ter tipos dinâmicos não significa que o Clojure não tenha tipos. Ele apenas não exige que eles sejam todos declarados antecipadamente. O mesmo problema de estender funções antigas para novos tipos, e novas funções para tipos antigos — estendendo a tabela de Wadler em ambas as direções — permanece.

Um exemplo concreto

O Problema da Expressão não é apenas sobre tipos abstratos como listas e conjuntos. Se você gastou uma quantidade significativa de tempo trabalhando com linguagens orientadas a objetos, provavelmente se deparou com exemplos do Problema da Expressão. Esta seção fornece um caso concreto, embora simplificado, do mundo real.

Suponha que você trabalhe no departamento de TI na WidgetCo, uma empresa de fornecimento de serviços de correio. A WidgetCo escreveu seu próprio software de faturamento e gerenciamento de inventário na linguagem Java.

Os produtos da WidgetCo são todos descritos por uma interface simples:

package com.widgetco;

public interface Widget {
    public String getName();
    public double getPrice();
}

Para cada tipo único de widget fabricado pela WidgetCo, os programadores do departamento de TI escrevem uma classe que implementa o Widget .

Uma ordem de um dos clientes da WidgetCo é implementada como uma lista de objetos Widget , com um método extra para calcular o custo total do pedido:

package com.widgetco;

public class Order extends ArrayList<Widget> {
    public double getTotalCost() { /*...*/ }
}

Tudo funciona bem na WidgetCo, até que a empresa é comprada pela Amagalmated Thingamabobs Incorporated. A Amalgamated tem seu próprio sistema de faturamento, também escrito na linguagem Java. Seu inventário é centrado em uma classe abstrata. Product, a partir da qual classes de produto específicas são derivadas:

package com.amalgamated;

public abstract class Product {
    public String getProductID() { /*...*/ }
}

Na Amalgamated, os produtos não têm preços fixos. Em vez disso, a empresa negocia com cada cliente para entregar uma certa quantidade de itens a um dado preço. Esse acordo é representado pela classe Contract :

package com.amalgamated;

public class Contract {
    public Product getProduct() { /*...*/ }
    public int getQuantity()    { /*...*/ }
    public double totalPrice()  { /*...*/ }
    public String getCustomer() { /*...*/ }
}

Depois da fusão, seus novos chefes na Amagalmated atribuem-lhe uma tarefa de escrever um novo aplicativo para gerar faturas e enviar declarações para a empresa fundida. Mas há uma armadilha: o novo sistema precisa funcionar com o código Java existente usado para o gerenciamento de inventário tanto na WidgetCo quanto com a Amalgamated Thingamabobs — as classes com.widgetco.Widget, com.widgetco.Order, com.amalgamated.Productecom.amalgamated.Contract . Muitos outros aplicativos dependem desse código para arriscar alterá-lo.

Você acaba de conhecer o Problema da Expressão.


"Soluções" em potencial

Linguagens orientadas a objeto oferecem diversas abordagens possíveis para resolver o Problema da Expressão, mas cada um tem suas desvantagens. Você provavelmente encontrou exemplos de cada uma dessas técnicas.

Herança de uma superclasse comum

A solução orientada a objeto tradicional para problemas desse tipo é aproveitar o polimorfismo de subtipo — ou seja, a herança. Se duas classes precisam dar suporte à mesma interface, ambas devem estender a mesma superclasse. Em meu exemplo, com.widgetco.Order e com.amalgamated.Contract precisam produzir faturas e declarações. De maneira ideal, ambas as classes implementariam alguma interface com os métodos necessários:

public interface Fulfillment {
    public Invoice invoice();
    public Manifest manifest();
}

Para fazer isso, é preciso modificar o código de origem para Order e Contract para implementar a nova interface. Mas essa era a armadilha neste exemplo: não se pode modificar o código de origem para essas classes. Não se pode sequer recompilá-las.

Herança múltipla

Outra abordagem ao Problema da Expressão é herança múltipla, na qual uma subclasse pode estender muitas superclasses. Você deseja uma representação genérica de uma compra que pode ser com.widgetco.Order ou com.amalgamated.Contract. O seguinte pseudocódigo mostra como isso pode se parecer se a linguagem Java tiver múltipla herança:

public class GenericOrder
        extends com.widgetco.Order, com.amalgamated.Contract
        implements Fulfillment {

    public Invoice invoice() { /*...*/ }

    public Manifest manifest() { /*...*/ }
}

Mas a linguagem Java não suporta múltipla herança de classes concretas, e por um bom motivo: isso leva a hierarquias de classe complexas e às vezes imprevisíveis. A linguagem Java de fato dá suporte a múltiplas heranças de interfaces, então se Order e Contract foram interfaces, é possível fazer essa técnica funcionar. Mas, infelizmente, os autores originais de Order e Contract não tinham uma visão de futuro suficiente para basear o design em interfaces. Mesmo se tivessem, esta não seria uma solução real ao Problema da Expressão, já que não é possível adicionar a interface Fulfillment às classes Order e Contract existentes. Em vez disso, você criou a nova classe GenericOrder , que tem os mesmos problemas que wrappers, descritos a seguir.

Wrappers

Outra solução popular é escrever wrappers em torno de classes cujo comportamento se deseja modificar. A classe wrapper é construída com uma referência à classe original, e encaminha métodos para essa classe. O wrapper Java para a classe Order pode se assemelhar a isto:

public class OrderFulfillment implements Fulfillment {
    private com.widgetco.Order order;

    /* constructor takes an instance of Order */
    public OrderFulfillment(com.widgetco.Order order) {
        this.order = order;
    }

    /* methods of Order are forwarded to the wrapped instance */

    public double getTotalCost() {
        return order.getTotalCost();
    }

    /* the Fulfillment interface is implemented in the wrapper */
    public Invoice invoice() { /*...*/ }

    public Manifest manifest() { /*...*/ }
}

OrderFulfillment é um wrapper em torno de Order . A classe wrapper implementa a interface Fulfillment descrita anteriormente. Também copia e encaminha métodos definidos por Order . Por que Order estende ArrayList<Widget>, uma implementação correta da classe wrapper também precisaria copiar e encaminhar todos os métodos de java.util.ArrayList.

Quando criar outro wrapper para com.amalgamated.Contract que também implementa Fulfillment, você terá cumprido os requisitos para a tarefa — mas ao custo de maior complexidade. As classes wrapper são tediosas de escrever. (java.util.ArrayList tem mais de 30 métodos.) Pior que isso, elas quebram certos comportamentos que se espera das classes. Uma OrderFulfillment, embora implemente os mesmos métodos que Order , não é realmente uma Order . Não é possível acessá-la através de um ponteiro declarado como Order , nem passá-la para outro método esperando Order como argumento. Adicionando uma classe wrapper, quebra-se o polimorfismo de subtipo.

Ainda pior, as classes wrapper quebram a identity. Uma Order em wrapper em OrderFulfillment não é mais o mesmo objeto. Não se pode comparar uma OrderFulfillment com uma Order com o operador Java == e esperar que retorne true. Se você tentar substituir o método Object.equals em OrderFulfillment , de modo que seja "igual" à sua Order , quebra o contrato de Object.equals, que especifica que a igualdade deve ser simétrica. Como Order não sabe nada sobre OrderFulfillment, seu método equals sempre retornará false quando passada uma OrderFulfillment. Se for definida OrderFulfillment.equals de modo que possa retornar true quando passada uma Order , quebra-se a simetria de equals, provocando uma cascata de falhas em outras classes, como as classes de coleção Java integradas, que dependem desse comportamento. A moral da história é: não brinque com a identidade.

Classes abertas

As linguagens Ruby e JavaScript ajudaram a popularizar a ideia de classes abertas em programação orientada a objetos. Uma classe aberta não é limitada ao conjunto de métodos que foram implementados quando ela foi definida. Qualquer um pode "reabrir" a classe a qualquer momento para adicionar novos métodos ou mesmo substituir métodos existentes.

Classes abertas permitem um alto grau de flexibilidade e reutilização. Classes genéricas podem ser estendidas com funcionalidade específica para diferentes locais onde são usadas. Grupos de métodos implementando um aspecto em particular de comportamento podem ser reunidos em uma combinação que é adicionada a qualquer classe que precise desse comportamento. Se o exemplo deste artigo fosse escrito em Ruby ou JavaScript, você poderia simplesmente reabrir as classes Order e Contract para adicionar os métodos necessários.

O lado negativo de classes abertas, além da sua inexistência na maioria das linguagens de programação (incluindo a linguagem Java), é a falta de certeza trazida pela sua grande flexibilidade. Se você definir o método invoice em uma classe, não terá como saber que algum outro usuário dessa classe não definirá um método diferente e incompatível também chamado invoice. Esse é o problema de confrontos de nomes, e é difícil evitar em linguagens com classes abertas e nenhum mecanismo de espaço de nomes para métodos. Há um motivo pelo qual essa técnica é conhecida como "monkey patching." É simples e fácil de entender, mas é quase garantido que causará problemas mais tarde.

Condicionais e sobrecarga

Uma das soluções mais comuns para o Problema da Expressão é simples e antiga lógica if-then-else. Usando instruções condicionais e verificações de tipo, enumera-se todo caso possível e se lida com cada um adequadamente. Para o exemplo da fatura, na linguagem Java, seria possível implementar isso em um método estático:

public class FulfillmentGenerator {
    public static Invoice invoice(Object source) {
        if (source instanceof com.widgetco.Order) {
            /* ... */
        } else if (source instanceof com.amalgamated.Contract) {
            /* ... */
        } else {
            throw IllegalArgumentException("Invalid source.");
        }
    }
}

Como a classe wrapper, essa técnica cumpre seus requisitos, mas vem com suas próprias desvantagens. A cadeia de blocos if-else fica mais confusa, e mais lenta, quanto mais tipos precisam ser tratados. E essa implementação é fechada: Depois de compilar a classe FulfillmentGenerator , não é possível estender o método invoice para novos tipos sem editar o código de origem e recompilar. A solução condicional para o Problema da Expressão na verdade não é solução alguma, apenas um ardil que levará a mais trabalho de manutenção no futuro.

Neste caso, seria possível evitar a lógica condicional sobrecarregando o método invoice para diferentes tipos:

public class FulfillmentGenerator {
    public static Invoice invoice(com.widgetco.Order order) { 
        /* ... */
    }

    public static Invoice invoice(com.amalgamated.Contract contract) {
        /* ... */
    }
}

Isso obtém o mesmo resultado e é mais eficiente que a versão condicional, mas ainda não terminou: não é possível adicionar novas implementações de invoice para diferentes tipos sem modificar a origem de FulfillmentGenerator. Também se torna imprevisível face às hierarquias de herança. Suponha que você deseja adicionar uma implementação de invoice para uma subclasse do Order ou o Contract. Apenas um especialista em linguagem Java poderia dizer a você qual versão do método de fato seria chamada, e pode não ser aquela que você deseja.

Soluções reais ao Problema da Expressão na linguagem Java de fato existem (consulte Recursos ), mas são muito mais complexos que você gostaria de fazer para resolver um simples problema de negócios. Em geral, todas as não soluções apresentadas aqui falham porque unem tipos com outras coisas: herança, identidade ou espaços de nomes. O Clojure trata cada problema separadamente.


Protocolos

O Clojure 1.2 apresentou protocolos. Embora não seja uma nova ideia — cientistas da computação estavam pesquisando ideias similares nos anos 70— , a implementação do Clojure é flexível o suficiente para resolver o Problema da Expressão ao mesmo tempo em que preserva o desempenho da sua plataforma hospedada, a JVM.

Conceitualmente, um protocolo é como uma interface Java. Ele define o conjunto de nomes de método e suas assinaturas de argumento, mas não implementações. O exemplo da fatura pode se parecer com a Listagem 1:


Listagem 1. O protocolo Fulfillment
(ns com.amalgamated)

(defprotocol Fulfillment
  (invoice [this] "Returns an invoice")
  (manifest [this] "Returns a shipping manifest"))

Cada método de protocolo leva pelo menos um argumento, comumente chamado de neste. Com métodos Java, métodos de protocolo são chamados "em" um objeto, e o tipo desse objeto determina qual implementação do método é usada. Diferente da linguagem Java, o Clojure exige que this seja declarado como um argumento explícito para a função.

Os protocolos diferem de interfaces no sentido de que seus métodos existem, como funções ordinárias, no momento em que são definidos. O exemplo da Listagem 1 define funções do Clojure chamadas invoice e manifest, ambas no espaço de nomes com.amalgamated . É claro, essas funções não têm qualquer implementação ainda, assim, chamá-las apenas lançará uma exceção.

Os protocolos permitem fornecer implementações para seus métodos em uma base caso a caso. Você estende o protocolo para novos tipos, usando uma função chamada extend. A função extend assume um tipo de dados , um protocolo e um mapa de implementações de método. Explicarei os tipos de dados na próxima seção, mas, por enquanto, apenas suponha que tipos de dados são classes Java ordinárias. É possível estender o protocolo Fulfillment para funcionar na antiga classe Order , como mostrado na Listagem 2:


Listagem 2. Estendendo o protocolo Fulfillment para funcionar na classe Order
(extend com.widgetco.Order
  Fulfillment
  {:invoice (fn [this]
              ... return an invoice based on an Order ... )
   :manifest (fn [this]
               ... return a manifest based on an Order ... )})

Observe que o mapa passado para a função extend mapeia desde palavras-chave a funções anônimas. As palavras-chave são nomes dos métodos no protocolo; as funções são implementações desses métodos. Chamando extend, você está indicando ao protocolo Fulfillment : "Aqui está o código para implementar esses métodos no tipo com.widgetco.Order ."

É possível fazer o mesmo para a classe Contract , como mostrado na Listagem 3:


Listagem 3. Estendendo o protocolo Fulfillment para trabalhar na classe Contract class
(extend com.amalgamated.Contract
  Fulfillment
  {:invoice (fn [this]
              ... return an invoice based on a Contract ... )
   :manifest (fn [this]
               ... return a manifest based on a Contract ... )})


E com isso, você tem funções polimórficas invoice e manifest que podem ser chamadas em uma Order ou uma Contract e que farão a coisa certa. Você satisfez a parte difícil do Problema da expressão: adicionou as novas funções invoice e manifest aos tipos Order e Contract preexistentes sem modificar ou recompilar qualquer código existente.

Diferente da abordagem de classes abertas, você não alterou as classes Order e Contract . Nem impediu que outro código definisse seus próprios métodos invoice e manifest com diferentes implementações. Seus métodos invoice e manifest são espaços de nomes dentro de métodos com.amalgamated; métodos definidos em outros espaços de nome nunca ficarão em oposição.

O Clojure, sendo um Lisp, usa macros para simplificar alguma sintaxe complexa ou repetitiva, Usando o macro integrado extend-protocol , é possível escrever todas as implementações de método em um bloco, como mostrado na Listagem 4:


Listagem 4. Usando macro extend-protocol
(extend-protocol Fulfillment
  com.widgetco.Order
    (invoice [this]
      ... return an invoice based on an Order ... )
    (manifest [this]
      ... return a manifest based on an Order ... )
  com.amalgamated.Contract
    (invoice [this]
      ... return an invoice based on a Contract ... )
    (manifest [this]
      ... return a manifest based on a Contract ... ))

Esse código macro expande-se para as mesmas duas chamadas para extend mostradas na Listagem 2 e na Listagem 3.


Tipos de dados

Os protocolos são uma ferramenta potente: efetivamente proporcionam a capacidade de inserir novos métodos nas classes existentes, sem conflitos de nome e sem modificar o código original. Mas apenas resolvem metade do Problema da Expressão, as "novas colunas" da tabela de Wadler. Como adicionar "novas linhas" à tabela no Clojure?

A resposta é: tipos de dados. No panorama orientado a objetos, tipos de dados cumprem a mesma função que classes: encapsulam estado (campos) e comportamento (métodos). Entretanto, as principais linguagens orientadas a objeto, como a linguagem Java, tendem a unir as diferentes funções que as classes podem cumprir em um design orientado a objetos. Os tipos de dados do Clojure são divididos em duas funções distintas.

Dados estruturados: defrecord

Um uso para classes é como contêineres para dados estruturados. É onde todo livro-texto de programação orientada a objetos começa: você tem uma classe Person com campos como Nome e Age. Se os campos são acessados diretamente ou através de métodos getter/setter é imaterial; a classe em si cumpre a função de uma struct em C. Seu objetivo é conter um conjunto de valores logicamente relacionados. JavaBeans são exemplos de classes usadas para conter um dado estruturado.

O problema com o uso de classes para dados estruturados é que cada classe possui sua própria interface distinta para acessar esses dados, tipicamente através de métodos getter/setter. O Clojure favorece interfaces uniformes, então incentiva o armazenamento de dados estruturados em mapas. Implementações de mapa do Clojure — HashMap, ArrayMapeStructMap — todas dão suporte à interface de manipulação de dados Discuti no início deste artigo. Mas mapas carecem de alguns dos recursos esperados de dados estruturados: eles não possuem "tipo" real (sem metadados adicionados) e, diferente de classes, não podem ser estendidos com novo comportamento.

O macro defrecord , apresentado junto com protocolos no Clojure 1.2, pode ser usado para criar contêineres para dados estruturados que combinam os recursos de mapas e classes.

Retornando ao exemplo de execução deste artigo, suponha que a gerência da Amalgamated Thingamabobs Incorporated decide que é hora de uniformizar os processos de compra dos seus clientes. Deste ponto em diante, novos pedidos de produtos serão representados por um objeto PurchaseOrder , e as antigas classes Order e Contract serão gradualmente canceladas.

Um PurchaseOrder terá propriedades de "data", "cliente" e "produtos". Também deve fornecer implementações de invoice e manifest. Como o Clojure defrecord, se parece com a Listagem 5:


Listagem 5. O tipo de dado PurchaseOrder
(ns com.amalgamated) (defrecord PurchaseOrder [date customer products]
  Fulfillment
    (invoice [this]
      ... return an invoice based on a PurchaseOrder ... )
    (manifest [this]
      ... return a manifest based on a PurchaseOrder ... ))


O vetor após o nome PurchaseOrder define os campos do tipo de dados. Depois dos campos, defrecord permite escrever definições sequenciais para métodos de protocolo. A Listagem 5 implementa apenas um protocolo, Fulfillment, mas poderia ser seguida de qualquer número de outros protocolos e suas implementações.

Sob o capô, defrecord cria uma classe Java com os campos date, customereproducts . Para criar uma nova instância de PurchaseOrder, chama-se seu construtor da maneira usual, fornecendo valores para os campos, como mostrado na Listagem 6:


Listagem 6. Criando uma nova instância de PurchaseOrder
(def po (PurchaseOrder. (System/currentTimeMillis)
                        "Stuart Sierra"
                        ["product1" "product2"]))

Quando a instância é construída, chamam-se as funções invoice e manifest no seu objeto PurchaseOrder , que usa as implementações fornecidas em defrecord. Mas também é possível tratar o objeto PurchaseOrder como um mapa do Clojure: defrecord automaticamente adiciona os métodos necessários para implementar a interface do mapa do Clojure. É possível recuperar campos por nome (como palavras-chave), atualizar esses campos e inclusive adicionar novos campos não na definição original, — tudo usando as funções de mapa padrão do Clojure, como na seguinte sessão REPL:

com.amalgamated> (:date po)
1288281709721
com.amalgamated> (assoc po :rush "Extra Speedy")
#:com.amalgamated.PurchaseOrder{:date 1288281709721,
                                :customer "Stuart Sierra",
                                :products ["product1" "product2"],
                                :rush "Extra Speedy"}
com.amalgamated> (type po)
com.amalgamated.PurchaseOrder

Observe que quando um tipo de registro é impresso, ele parece um mapa com uma identificação de mapa extra na frente. Esse tipo pode ser recuperado da função type . O fato de que mapas e tipos de registro obedecem a mesma interface é especialmente conveniente durante o desenvolvimento: é possível iniciar com mapas ordinários e alternar para tipos de registro quando precisar de recursos extras, sem quebrar seu código.

Comportamento puro: deftype

Nem todas as classes representam dados estruturados no domínio do aplicativo. O outro tipo de classe normalmente representa objetos específicos a uma implementação, como coleções (por exemplo, ArrayList, HashMap, SortedSet) ou valores (por exemplo, String, Data, BigDecimal). Essas classes implementam suas próprias interfaces genéricas, assim, não faz sentido para elas comportarem-se como mapas Clojure. Para cumprir essa função, o Clojure oferece uma variante de tipo de dados para elementos que não são registros. O macro deftype tem a mesma sintaxe que defrecord, mas cria uma classe Java exposta, sem qualquer recurso tipo mapa. Qualquer comportamento da classe deve ser implementado como métodos de protocolo. Você tipicamente usa deftype para implementar novos tipos de coleções ou para definir completamente novas abstrações em torno de protocolos que você especifica.

Um recurso que é deliberadamente deixado de fora de tipos de dados é habilidade de implementar métodos que não sejam definidos em nenhum protocolo. Um tipo de dados deve completamente especificado por seus campos e os protocolos que implementa; isso garante que as funções chamadas em tipo de dados sejam sempre polimórficas. Qualquer tipo de dados pode ser substituído por outro tipo de dados, desde que implemente os mesmos protocolos.


Interagindo com o código Java

Nos últimos meses, você conseguiu trazer algum Clojure para os sistemas de produção da Amalgamated Thingamabobs Incorporated. Porque ele foi compilado em arquivos JAR, as outras equipes de programação sequer perceberam. Mas então outra equipe de desenvolvedores apenas Java precisa construir outro gerador de fatura que seja compatível com o código Clojure. Você começa a falar com eles sobre Clojure, Lisp, e o Problema da Expressão, e os olhos deles ficam vidrados.

Por fim, você diz: "Sem problemas, apenas façam as classes Java implementarem esta interface", e mostra-lhes isto:

package com.amalgamated;

public interface Fulfillment {
    public Object invoice();
    public Object manifest();
}

O Clojure já compilou o protocolo definido anteriormente para o bytecode da JVM representando esta interface. Na verdade, o protocolo every do Clojure é também uma interface Java com o mesmo nome e métodos. Se você tiver cuidado com a nomenclatura (ou seja, não usar caracteres nos nomes de método que não sejam válidos na linguagem Java), outros desenvolvedores Java podem implementar a interface sem sequer saber que foi gerada no Clojure.

O mesmo é verdade para tipos de dados, embora eles gerem classes que se pareçam um pouco menos com o que um desenvolvedor Java esperaria. O tipo de dados PurchaseOrder definido na Listagem 5 gera bytecode para uma classe como esta:

public class PurchaseOrder
    implements Fulfillment, 
               java.io.Serializable, 
               java.util.Map,
               java.lang.Iterable,
               clojure.lang.IPersistentMap {

    public final Object date;
    public final Object customer;
    public final Object products;

    public PurchaseOrder(Object date, Object customer, Object products) {
        this.date = date;
        this.customer = customer;
        this.products = products;
    }
}

Os campos do tipo de dados são declarados public final na manutenção com a política do Clojure dos dados imutáveis. Eles só podem inicializados através do construtor. Você pode ver que essa classe implementa Fulfillment . As outras interfaces são as bases das APIs de manipulação de dados do Clojure — elas estariam ausentes em um tipo de dados criados com deftype em vez de defrecord. Observe que os tipos de dados podem implementar métodos de qualquer interface Java, não apenas aquelas geradas por protocolos Clojure.

O Clojure tem outras formas de interoperabilidade Java que produz código mais idiomático no Java — mas menos idiomático no Clojure — quando essa é a única opção.


conclusão

O Problema da Expressão é um problema prático real em programação orientada a objeto e a maioria das soluções comuns para ele é inadequada. Os protocolos e tipos de dados do Clojure fornecem uma solução simples e elegante. Os protocolos permitem que você defina funções polimórficas sobre os tipos preexistentes. Os tipos de dados permitem criar novos tipos que suportem funções preexistentes. Juntos, permitem estender seu código em ambas as direções, como mostrado na Figura 3:


Figura 3. Clojure: Fácil de adicionar novas colunas (protocolos) e linhas (tipos de dados)
Image showing the final version of the table, adapted from Figures 1 and 2. The columns represent any functions or methods that already exist. Rows represent classes (Java) and datatypes (Clojure) that already exist. The intersection of those rows and columns represents all preexisting implementations. You can add a new Clojure datatype as a new row at the bottom of the table, and a new Clojure protocol as a new column at the right side of the table. Your new protocol can be extended to your new datatype, filling in the new bottom-right cell.

A Figura 3 mostra a versão final da tabela, adaptada da Figura 1 e da Figura 2. As colunas representam quaisquer funções ou métodos já existentes. As linhas representam classes (Java) e tipos de dados (Clojure) que já existem. A interseção dessas linhas e colunas representam todas as implementações preexistentes, que você não deseja ou precisa alterar. É possível adicionar um novo tipo de dados do Clojure como uma nova linha na parte inferior da tabela e um novo protocolo do Clojure como uma nova coluna no lado direito da tabela. Além disso, seu novo protocolo pode ser estendido para seu novo tipo de dados, preenchendo na nova célula inferior direita.

Soluções para o Problema da Expressão, além daquelas descritas aqui, estão disponíveis. O Clojure em si sempre teve múltiplos métodos, que fornecem uma variedade ainda mais flexível de polimorfismo (embora um que tenha um desempenho menor). Múltiplos métodos são similares às funções genéricas em outras linguagens, como Common Lisp. Os protocolos do Clojure também têm uma semelhança com as classes de tipo da Haskell.

Todo programador encontrará o Problema da Expressão mais cedo ou mais tarde, então toda linguagem de programação tem algum tipo de resposta para ele. Como um experimento interessante, escolha qualquer linguagem de programação — que conheça bem ou uma que esteja recém aprendendo — e pense sobre como poderia usá-la para solucionar um problema como o exemplo apresentado neste artigo. É possível fazer isso sem modificar o código existente? É possível fazer isso sem quebrar a identidade? É possível ter certeza de que está seguro quanto a confrontos de nome? E, mais importante, é possível fazer isso sem criar um problema ainda maior no futuro?


Recursos

Aprender

Obter produtos e tecnologias

Discutir

  • Participe do comunidade My developerWorks. Entre em contato com outros usuários do developerWorks e explore os blogs, fóruns, grupos e wikis voltados para desenvolvedores.

Sobre o autor

Stuart Sierra

Stuart Sierra é ator/escritor/codificador que vive em na cidade de Nova York. Ele é membro da equipe Clojure/core em Relevance, Inc. Stuart é coautor de Practical Clojure (Apress, 2010). Ele é mestre em ciência da computação pela Columbia University e bacharel em teatro pela New York University.

Ajuda para Relatar Abuso

Relatar abuso

Obrigado. Esta entrada foi sinalizada para atenção do moderador.


Ajuda para Relatar Abuso

Relatar abuso

Falha no envio do Relatório de abuso. Tente novamente mais tarde.


developerWorks: Registre-se


Precisa de um ID IBM?
Esqueceu seu ID IBM?


Esqueceu sua senha?
Alterar sua senha

Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Na primeira vez que você efetua sign in no developerWorks, um perfil é criado para você. Informações selecionadas do seu perfil developerWorks são exibidas ao público, mas você pode editá-las a qualquer momento. Seu primeiro nome, sobrenome (a menos que escolha ocultá-los), e seu nome de exibição acompanharão o conteúdo que postar.

Selecione seu nome de exibição

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.

(Deve possuir de 3 a 31 caracteres.)


Ao clicar em Enviar, você concorda com os termos de uso do developerWorks.

 


Classificar este artigo

Comentários

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=80
Zone=Tecnologia Java
ArticleID=630538
ArticleTitle=Resolvendo o problema da expressão com Clojure 1.2
publish-date=12142010
author1-email=stuart.sierra@thinkrelevance.com
author1-email-cc=jaloi@us.ibm.com

Conheça a IBM da sua cidade

Virtual Branch Office Brasil

A IBM está mais perto do que você imagina!


Tags

Help
Use o campo de pesquisa para encontrar todos os tipos de conteúdo no My developerWorks com essa tag.

Use a barra de rolagem para ver mais ou menos tags.

Tags populares mostra as principais tags para esta zona de conteúdo em particular (por exemplo, Java technology, Linux, WebSphere).

Minhas tags mostra suas tags para esta zona de conteúdo em particular (por exemplo, Java technology, Linux, WebSphere).

Use o campo de pesquisa para localizar todos os tipos de conteúdo no Meu developerWorks com essa tag. Tags populares mostra as tags principais para essa zona de conteúdo particular (por exemplo, tecnologia Java, Linux, WebSphere). My tags shows your tags for this particular content zone (for example, Java technology, Linux, WebSphere). Minhas tags mostra as suas tags para essa zona de conteúdo em particular (por exemplo, tecnologia Java, Linux, WebSphere).