29 de fevereiro de 2012

Levando o conhecimento de Haskell para Java

Resgatando o blog e trocando um pouco o tema para falar sobre programação.

Existem muitos tutoriais sobre orientação a objetos e desenvolvimento orientado por testes (TDD) pela internet. Grande parte se utiliza de um exemplo simples: uma calculadora.

Para desenvolver a calculadora, demonstram o poder do polimorfismo construindo abstrações sobre operações binárias e a utilidade do TDD através de uma programação incremental.

Neste post tentarei extender este exemplo de calculadora, porém focando em outro ponto do problema: a interação com o usuário.

Os exemplos estão na linguagem orientada a objetos mais utilizada do planeta: Java.

  • Programa: Calculadora para terminal.
  • Requisito: Ler cálculos no formato "2 + 2" e informar o usuário sobre o resultado do cálculo: "4".
  • Problema: Como testar a interação com o usuário?

Por ser um programa feito para rodar em terminal, utilizaremos System.in e System.out para interagir com o usuário. Porém, se amarrarmos o algoritmo de cálculo diretamente à estes recursos, ficará muito difícil e não-produtivo criar e manter testes para este sistema.

Algo importante que aprendi com a linguagem Haskell é a de separar o código em áreas puras e impuras. Uma área de código pura é um conjunto de funções que dependem apenas de seus argumentos e só produzem um resultado, i.e. funções que não alteram nenhuma informação e dependem apenas de seus argumentos. A área de código impura é aquela composta pelas demais funções que dependem de I/O, alterações na memória, banco de dados, etc.

A linguagem Haskell faz um trabalho incrível para assegurar que esta distinção seja mantida pelos programadores através do seu sistema de tipos que diferenciam funções puras e funções impuras. Nas linguagens tradicionais, como Java, não existe esta distinção: todo pedaço de código tem a liberdade de ser impuro. Desta forma, os programadores devem sempre prestar atenção para o que cada parte do código está executando.

Para separar a parte "pura" e "impura" da nossa calculadora, precisamos primeiro identificar quais funções pertencem a cada parte.

Toda a parte de código que realiza o cálculo pode se encaixar perfeitamente na área de código pura, afinal são apenas representações para funções matemáticas, que são puras por definição.

Por outro lado, a parte que faz a interação com o usuário é impura, pois altera informações que vão além de seu alcance em termos de código: imprime caracteres no terminal, e recebe informações que vão além de seus argumentos: teclas pressionadas pelo usuário.

A parte impura do código é muito difícil de ser testada e por isto deve ser mantida reduzida e de forma controlada, que faça apenas o necessário para o funcionamento. No caso da calculadora para terminal, a parte impura é apenas interação com o usuário no seguinte formato:

  1. Usuário digita um texto.
  2. Programa responde com outro texto.
  3. Volte para 1.

Esta interação é abstrata o suficiente para ser totalmente separada da parte que realiza o processamento. Por exemplo, o seguinte código encapsularia toda a parte impura da nossa calculadora:

public interface Programa {
  String processar(String entrada);
}

public class InteracaoTerminal {
  public void interagir(Programa programa) {
    Scanner s = new Scanner(System.in);
    while (true) {
      String entrada = s.nextLine();
      String saida = programa.processar(entrada);
      System.out.println(saida);
    }
  }
}

Desta forma a interface Programa é 100% testavel, por exemplo:

assertEquals("4", programa.processar("2 + 2"));
assertEquals("6", programa.processar("2 + 2 * 2"));

Além de testavel, a implementação da nossa calculadora apenas manipulará Strings, sem dependência direta com o System. Isto significa que poderemos aproveitar o código em outros lugares, quem sabe utilizar o mesmo código para duas interfaces diferentes: uma interage com o usuário através de um terminal e outra através de um aplicativo gráfico.

A abstração colocada como exemplo tem uma limitação: cada interação com o usuário é feita através de uma linha de entrada e uma linha de saída. Deixo como exercício aprimorar esta abstração para que seja possível ter mais de uma linha de entrada e mais de uma linha como saída de uma única interação. Se você conhece Haskell, poderá se basear na função interact, e se você não conhece: Aprender Haskell será um grande bem para você!