Artigo Java Magazine 56 - Programação Funcional (ou quase) em Java

Saiba como aplicar o estilo de programação funcional em Java, e examinando a linguagem Scala.

Esse artigo faz parte da revista Java Magazine edição 56. Clique aqui para ler todos os artigos desta edição

Clique aqui para ler esse artigo em PDF.

Programação Funcional (ou quase) em Java

Saiba como – e porquê – adotar técnicas de Programação Funcional na plataforma Java

Aplicando o estilo de programação funcional em Java, e examinando a linguagem Scala

O leitor que acompanha esta coluna já deve conhecer meu apreço por técnicas de programação funcional, que tenho comentado em diversos artigos, por exemplo, ao falar de concorrência e outras situações beneficiadas por esse paradigma. Muitos leitores devem imaginar: esse Osvaldo deve ser um veterano de LISP ou Scheme, que nunca se conformou com linguagens imperativas... longe da verdade. É fato que experimentei algumas linguagens funcionais, quase sempre em cursos universitários. Não cheguei a dominar nenhuma linguagem dessas, o que é impossível sem uma boa experiência prática, desenvolvendo aplicações reais. Sendo que o mercado de desenvolvimento de software no qual atuo dá pouca oportunidade para aventuras com linguagens alternativas.

O que acontece é o oposto – sou um especialista em linguagens OO; antes do Java programei muito em C++, também estudei outras como Eiffel e Smalltalk. Durante anos, acreditei que a POO era o Alfa e o Ômega, a solução suficiente para todos os problemas da programação. Com o tempo, fui vendo que não era bem assim; fui me inteirando de uma realidade mais rica. E tentando integrá-la à minha prática profissional – independente da linguagem utilizada.

Como já gastei algumas páginas desta revista com propaganda do paradigma funcional, resolvi dar o serviço completo e explicar o estilo de programação Java funcional “light” que tenho aplicado cada vez mais aos meus projetos. Pra completar, termino com uma olhada preliminar de Scala, uma nova linguagem de programação para a plataforma Java SE que combina as vantagens de OO e programação funcional.

Por quê a programação funcional importa?

Para começar fundamentando minha tese, não há palavras melhores do que as escritas por John Hughes em seu célebre paper de 1984, Why Functional Programming Matters. Resumo aqui somente o essencial. Na verdade, vou resumir tanto que só apresentarei um único argumento.

A maior vantagem da programação funcional é sua modularidade. Em toda a evolução das LPs, vemos que essa qualidade é fundamental. Na evolução de linguagens “espaguete” como Fortran para as estruturadas como C e Pascal, as novas linguagens permitiam organizar um programa em procedimentos e estruturas de dados customizadas. Estes artefatos bem definidos e isolados ficavam mais simples de entender, reusáveis, fáceis de dar manutenção, etc.

No próximo passo evolutivo, as linguagens OO investiram novamente na modularidade. Uma classe permite unificar, de forma muito poderosa, algoritmos e dados relacionados. O polimorfismo potencializa essa modularização, permitindo que o cliente de uma classe manipule de maneira uniforme toda uma hierarquia de classes derivadas. Por exemplo, um método iterator() pode ser usado de forma homogênea para centenas de classes compatíveis com Collection.

Estes avanços foram importantes, mas agem numa única dimensão: a estrutural. Substituímos variáveis globais por parâmetros, GOTO por WHILE, cascatas de ifs ou switch por polimorfismo, etc. Mas no fundo, estamos programando do mesmo jeito que antes, só com mais ordem e facilidade.

Estado mutável

Há outra dimensão na qual podemos avançar na modularidade. Começaremos definindo o conceito de estado. Um processo possui um estado, que podemos definir, informalmente, como o conjunto de valores de todas as variáveis existentes a qualquer momento. Mais precisamente: o conteúdo total do heap, e dos stacks de todos os threads. Em linguagens OO este estado é tipicamente encapsulado em muitas pequenas partes; em especial, cada objeto mutável (que possui atributos cujos valores podem ser alterados) possui seu próprio estado, que constitui um pedacinho minúsculo do estado do programa inteiro.

Um efeito colateral é toda ação do programa que produz alteração do seu estado. Num código como x = 10; ++x, a segunda instrução, o ++x, é um efeito colateral. Também chamamos isso de atribuição destrutiva, pois “destrói” o valor anterior (10) que estava associado à variável x.

O leitor atento pode imaginar que o x = 10 também altera o estado do programa, pois cria uma variável que não existia antes. Mas isso não é verdade, de um ponto de vista abstrato, matemático. Considere esta instrução como um simples binding: uma associação nome?valor. Imagine um heap infinito, onde todos os objetos possíveis existem o tempo todo: por exemplo, todos os 232 inteiros possíveis, todas as Strings com todas as infinitas combinações de caracteres, etc. Neste modelo teórico, a única coisa que um binding faz é dar nome aos bois; associar a certo objeto um identificador que permite referenciá-lo. É claro que no mundo real da implementação de qualquer linguagem, essa abstração é quebrada, pois qualquer valor tem que ser alocado na memória e inicializado num determinado instante, e pode deixar de existir no instante seguinte. Porém, isso é apenas um detalhe de implementação.

Para a questão de modularidade, somente as atribuições destrutivas são realmente danosas, vejamos o porquê. Quando você pensa na interface pública de um método ou objeto, parece que esta é definida somente pelos seus parâmetros, tipo de retorno, e no caso do Java suas exceções. Mas na verdade, o método pode ter também dependências do estado do programa. E pode modificar esse estado, alterando atributos de objetos. Assim, a interface real dos seus métodos e objetos acaba sendo bem maior do que parece, o que aumenta o acoplamento do código e reduz a modularidade.

Modularidade é praticamente sinônimo de baixo acoplamento: dividir o programa em pedaços pequenos, independentes, fáceis de combinar e reusar em vários contextos.

O termo “efeito colateral” é utilizado porque as alterações de estado permitem interferências indiretas entre diversos componentes de um programa. Se você tem um objeto A cujo comportamento depende do estado de um objeto B, e um objeto C que invoca métodos de B que alteram esse estado, então isso induz uma alteração de comportamento em Asem que C jamais tenha se comunicado diretamente com A.

Uma conseqüência notável da programação baseada em efeitos colaterais é que somos obrigados a micro-gerenciar o tempo e a ordem das operações. Temos que determinar o momento exato em que qualquer mudança de estado acontece, o que complica a programação desnecessariamente e bloqueia técnicas importantes, como veremos.

Outro aspecto da modularidade de LFs, discutido no paper de Hughes, mas não aqui, é a composição de funções. Linguagens funcionais facilitam a criação de funções compostas a partir de funções preexistentes, explorando avaliação lazy, computação “high-order”, e outras técnicas.

Estado Gerenciado

Para não dizerem que me limitei a papagaiar um paper escrito há um quarto de século, proponho outro ponto de vista. O conceito de “memória gerenciada” é bem conhecido, graças às plataformas Java e .NET. Nestas plataformas, não se pode fazer coisas como acessar a memória arbitrariamente, ou realizar operações inválidas sobre um objeto. Por exemplo, não existem operações como delete ou free() para desalocar objetos; mas temos o Garbage Collector, que faz isso automaticamente (e sem risco de erros). Da mesma forma, é impossível realizar typecasts ilegais.

Após décadas de competição entre as linguagens de baixo nível com gerenciamento de memória manual e livre, e as de alto nível com memória automática e type-safe, a opção gerenciada venceu. Não há quase nenhuma linguagem moderna que não seja assim. Todas as linguagens de que você ouve falar ultimamente – Java, C#, Ruby, Python, Groovy, Javascript, Scala, Fortress, Haskell, etc., todas são gerenciadas. A memória manual é cada vez mais uma técnica de nicho, para programar sistemas operacionais, device drivers, firmware e coisas desse nível.

O exemplo da memória gerenciada é ideal, pois tem a ver com o estado do programa. Afinal, 99% do que chamamos “estado do programa” é o conteúdo do heap[1]. Se já temos memória automática do ponto de vista do gerenciamento de espaço (deleção de objetos inatingíveis) e segurança (operações type-safe), por que não dar um passo além e automatizar completamente as transições de estado?

Vamos tornar as coisas mais concretas com um exemplo. Desejamos um método reverse(), que recebendo uma lista, retorna outra lista com os mesmos elementos numa ordem inversa; ex.: reverse([a,b,c]) = [c,b,a]. Um programador Java provavelmente escreveria este método assim:

 

List reverse (List list) {

  List ret = new ArrayList(list.length);

  for (int i = list.size() - 1; i >= 0; --i)

    ret.add(list.get(i));

  return list;

}

 

Note que este código faz uso amplo de atribuições destrutivas. Primeiro no array ret, que é inicialmente alocado com null em todas as posições, sendo que estes null são posteriormente substituídos por valores. Segundo, na variável i, que controla o loop for.

Veja, agora, a versão típica de uma linguagem funcional, no caso Haskell:

 

reverse []       = []

reverse (a:x) = reverse x ++ [a]

 

Nesta versão, a iteração é substituída por recursão (na segunda linha). Por exemplo, reverse [a,b,c] resultaria em reverse [b,c] ++ [a], e assim por diante. O objetivo aqui não é mostrar que o código Haskell é mais simples ou mais curto – isso é em parte devido a características secundárias, como uma sintaxe mais enxuta para definir funções ou para manipular listas. Chamo a atenção do leitor para dois fatos mais importantes:" [...] continue lendo...

Ebook exclusivo
Dê um upgrade no início da sua jornada. Crie sua conta grátis e baixe o e-book

Artigos relacionados