Definição de Programação Funcional
A introdução de The Joy of Clojure diz o seguinte:
A programação funcional é um daqueles termos de computação que possui uma definição amorfa. Se você pedir a definição de 100 programadores, provavelmente receberá 100 respostas diferentes ...
A programação funcional diz respeito e facilita a aplicação e composição de funções ... Para que uma linguagem seja considerada funcional, sua noção de função deve ser de primeira classe. As funções de primeira classe podem ser armazenadas, transmitidas e retornadas como qualquer outro dado. Além desse conceito central, as [definições de PF podem incluir] pureza, imutabilidade, recursão, preguiça e transparência referencial.
Programando em Scala 2ª Edição p. 10 tem a seguinte definição:
A programação funcional é guiada por duas idéias principais. A primeira idéia é que funções são valores de primeira classe ... Você pode passar funções como argumentos para outras funções, retorná-las como resultados de funções ou armazená-las em variáveis ...
A segunda idéia principal da programação funcional é que as operações de um programa devem mapear valores de entrada para valores de saída, em vez de alterar os dados no local.
Se aceitarmos a primeira definição, a única coisa que você precisa fazer para tornar seu código "funcional" é transformar seus loops de dentro para fora. A segunda definição inclui imutabilidade.
Funções de Primeira Classe
Imagine que você obtenha atualmente uma Lista de passageiros do seu objeto Ônibus e itere sobre ele, diminuindo a conta bancária de cada passageiro pelo valor da tarifa do ônibus. A maneira funcional de executar essa mesma ação seria ter um método no barramento, talvez chamado forEachPassenger, que assume a função de um argumento. O Bus repetiria os passageiros, porém, da melhor maneira possível, e o código do cliente que cobra a tarifa do passeio seria colocado em uma função e passado para o forEachPassenger. Voila! Você está usando programação funcional.
Imperativo:
for (Passenger p : Bus.getPassengers()) {
p.debit(fare);
}
Funcional (usando uma função anônima ou "lambda" no Scala):
myBus = myBus.forEachPassenger(p:Passenger -> { p.debit(fare) })
Versão Scala mais açucarada:
myBus = myBus.forEachPassenger(_.debit(fare))
Funções não de primeira classe
Se o seu idioma não suportar funções de primeira classe, isso pode ficar muito feio. No Java 7 ou anterior, você deve fornecer uma interface "Objeto Funcional" como esta:
// Java 8 has java.util.function.Consumer, but in earlier
// versions you have to roll your own:
public interface Consumer<T> {
public void accept(T t);
}
Em seguida, a classe Bus fornece um iterador interno:
public void forEachPassenger(Consumer<Passenger> c) {
for (Passenger p : passengers) {
c.accept(p);
}
}
Por fim, você passa um objeto de função anônima para o barramento:
// Java 8 has syntactic sugar to make this look more like
// the Scala solution, but earlier versions require manually
// instantiating a "Function Object," in this case, a
// Consumer:
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
}
}
O Java 8 permite que variáveis locais sejam capturadas no escopo de uma função anônima, mas em versões anteriores, qualquer uma dessas varibales deve ser declarada final. Para contornar isso, pode ser necessário criar uma classe de wrapper MutableReference. Aqui está uma classe específica de número inteiro que permite adicionar um contador de loop ao código acima:
public static class MutableIntWrapper {
private int i;
private MutableIntWrapper(int in) { i = in; }
public static MutableIntWrapper ofZero() {
return new MutableIntWrapper(0);
}
public int value() { return i; }
public void increment() { i++; }
}
final MutableIntWrapper count = MutableIntWrapper.ofZero();
Bus.forEachPassenger(new Consumer<Passenger>() {
@Override
public void accept(final Passenger p) {
p.debit(fare);
count.increment();
}
}
System.out.println(count.value());
Mesmo com essa feiura, às vezes é benéfico eliminar a lógica complicada e repetida dos loops espalhados por todo o programa, fornecendo um iterador interno.
Essa feiúra foi corrigida no Java 8, mas o tratamento de exceções verificadas dentro de uma função de primeira classe ainda é realmente feio e o Java ainda carrega a suposição de mutabilidade em todas as suas coleções. O que nos leva a outros objetivos frequentemente associados ao FP:
Imutabilidade
O item 13 de Josh Bloch é "Prefere a imutabilidade". Apesar da conversa comum sobre o lixo, o OOP pode ser feito com objetos imutáveis, e isso o torna muito melhor. Por exemplo, String em Java é imutável. StringBuffer, OTOH precisa ser mutável para criar uma String imutável. Algumas tarefas, como trabalhar com buffers, inerentemente exigem mutabilidade.
Pureza
Cada função deve ser pelo menos memorizável - se você fornecer os mesmos parâmetros de entrada (e não deve ter entrada além dos argumentos reais), deve produzir a mesma saída todas as vezes sem causar "efeitos colaterais", como alterar o estado global, executando I / O ou lançando exceções.
Foi dito que na Programação Funcional, "geralmente é necessário algum mal para realizar o trabalho". 100% de pureza geralmente não é o objetivo. Minimizar os efeitos colaterais é.
Conclusão
Realmente, de todas as idéias acima, a imutabilidade foi a maior vitória em termos de aplicativos práticos para simplificar meu código - seja OOP ou FP. Passar funções para iteradores é a segunda maior vitória. A documentação do Java 8 Lambdas tem a melhor explicação do porquê. A recursão é ótima para o processamento de árvores. Preguiça permite que você trabalhe com coleções infinitas.
Se você gosta da JVM, recomendo que você dê uma olhada no Scala e Clojure. Ambas são interpretações perspicazes da Programação Funcional. O Scala é seguro para o tipo com sintaxe um pouco semelhante ao C, embora realmente tenha tanta sintaxe em comum com Haskell quanto com C. Clojure não é seguro para o tipo e é um Lisp. Recentemente, publiquei uma comparação de Java, Scala e Clojure em relação a um problema específico de refatoração. A comparação de Logan Campbell usando o Game of Life inclui Haskell e Clojure também.
PS
Jimmy Hoffa apontou que minha classe de ônibus é mutável. Em vez de consertar o original, acho que isso demonstrará exatamente o tipo de refatoração de que trata esta questão. Isso pode ser corrigido, tornando cada método no ônibus uma fábrica para produzir um novo ônibus, cada método no passageiro uma fábrica para produzir um novo passageiro. Portanto, adicionei um tipo de retorno a tudo o que significa que copiarei o java.util.function.Function do Java 8 em vez da interface Consumer:
public interface Function<T,R> {
public R apply(T t);
// Note: I'm leaving out Java 8's compose() method here for simplicity
}
Depois no ônibus:
public Bus mapPassengers(Function<Passenger,Passenger> c) {
// I have to use a mutable collection internally because Java
// does not have immutable collections that return modified copies
// of themselves the way the Clojure and Scala collections do.
List<Passenger> newPassengers = new ArrayList(passengers.size());
for (Passenger p : passengers) {
newPassengers.add(c.apply(p));
}
return Bus.of(driver, Collections.unmodifiableList(passengers));
}
Finalmente, o objeto de função anônima retorna o estado modificado das coisas (um novo barramento com novos passageiros). Isso pressupõe que p.debit () agora retorne um novo passageiro imutável com menos dinheiro que o original:
Bus b = b.mapPassengers(new Function<Passenger,Passenger>() {
@Override
public Passenger apply(final Passenger p) {
return p.debit(fare);
}
}
Espero que agora você possa tomar sua própria decisão sobre o quão funcional deseja tornar sua linguagem imperativa e decidir se seria melhor redesenhar seu projeto usando uma linguagem funcional. No Scala ou Clojure, as coleções e outras APIs são projetadas para facilitar a programação funcional. Ambos têm interoperabilidade Java muito boa, para que você possa misturar e combinar idiomas. De fato, para interoperabilidade Java, o Scala compila suas funções de primeira classe para classes anônimas que são quase compatíveis com as interfaces funcionais do Java 8. Você pode ler sobre os detalhes na seção Scala in Depth. 1.3.2 .