Estou tendo dificuldade em encontrar recursos sobre como escrever programas em um estilo funcional. O tópico mais avançado que pude encontrar discutido on-line foi o uso de tipagem estrutural para reduzir as hierarquias de classes; a maioria trata apenas de como usar o mapa / dobra / redução / etc para substituir loops imperativos.
O que eu realmente gostaria de encontrar é uma discussão aprofundada sobre uma implementação OOP de um programa não trivial, suas limitações e como refatorá-lo em um estilo funcional. Não apenas um algoritmo ou uma estrutura de dados, mas algo com vários papéis e aspectos diferentes - um videogame, talvez. A propósito, eu li Programação Funcional do Mundo Real de Tomas Petricek, mas fiquei querendo mais.
Respostas:
Definição de Programação Funcional
A introdução de The Joy of Clojure diz o seguinte:
Programando em Scala 2ª Edição p. 10 tem a seguinte definição:
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:
Funcional (usando uma função anônima ou "lambda" no Scala):
Versão Scala mais açucarada:
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:
Em seguida, a classe Bus fornece um iterador interno:
Por fim, você passa um objeto de função anônima para o barramento:
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:
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:
Depois no ônibus:
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:
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 .
fonte
Eu tenho experiência pessoal "realizando" isso. No final, eu não criei algo que fosse puramente funcional, mas criei algo com o qual estou feliz. Aqui está como eu fiz isso:
x
, faça com que o método seja passado emx
vez de chamarthis.x
.x.methodThatModifiesTheFooVar()
emfooFn(x.foo)
map
,reduce
,filter
, etc.Eu não conseguia me livrar do estado mutável. Era muito não-idiomático na minha linguagem (JavaScript). Mas, fazendo com que todo estado seja passado e / ou retornado, é possível testar todas as funções. Isso é diferente do OOP, onde a configuração do estado levaria muito tempo ou a separação de dependências geralmente exige a modificação do código de produção primeiro.
Além disso, posso estar errado sobre a definição, mas acho que minhas funções são referencialmente transparentes: Minhas funções terão o mesmo efeito, com a mesma entrada.
Editar
Como você pode ver aqui , não é possível criar um objeto verdadeiramente imutável em JavaScript. Se você é diligente e controla quem chama seu código, pode fazê-lo sempre criando um novo objeto em vez de alterar o atual. Não valeu o esforço para mim.
Mas se você estiver usando Java, poderá usar essas técnicas para tornar suas classes imutáveis.
fonte
Não acho que seja realmente possível refatorar completamente o programa - você teria que redesenhar e reimplementar no paradigma correto.
Vi a refatoração de código definida como uma "técnica disciplinada para reestruturar um corpo de código existente, alterando sua estrutura interna sem alterar seu comportamento externo".
Você poderia tornar certas coisas mais funcionais, mas ainda tem um programa orientado a objetos. Você não pode simplesmente mudar pequenos pedaços para adaptá-lo a um paradigma diferente.
fonte
Eu acho que essa série de artigos é exatamente o que você deseja:
Retrogames puramente funcionais
http://prog21.dadgum.com/23.html Parte 1
http://prog21.dadgum.com/24.html Parte 2
http://prog21.dadgum.com/25.html Parte 3
http://prog21.dadgum.com/26.html Parte 4
http://prog21.dadgum.com/37.html Acompanhamento
O resumo é:
O autor sugere um loop principal com efeitos colaterais (os efeitos colaterais devem ocorrer em algum lugar, certo?) E a maioria das funções retorna pequenos registros imutáveis, detalhando como eles mudaram o estado do jogo.
Obviamente, ao escrever um programa do mundo real, você misturará e corresponderá a vários estilos de programação, usando cada um onde for mais útil. No entanto, é uma boa experiência de aprendizado tentar escrever um programa da maneira mais funcional / imutável e também escrever da maneira mais espaguete, usando apenas variáveis globais :-) (faça como um experimento, não em produção, por favor)
fonte
Você provavelmente teria que inverter todo o código, pois OOP e FP têm duas abordagens opostas à organização do código.
OOP organiza o código em torno de tipos (classes): classes diferentes podem implementar a mesma operação (um método com a mesma assinatura). Como resultado, o OOP é mais apropriado quando o conjunto de operações não muda muito, enquanto novos tipos podem ser adicionados com muita frequência. Por exemplo, considere uma biblioteca GUI em que cada elemento tem um conjunto fixo de métodos (
hide()
,show()
,paint()
,move()
, e assim por diante), mas novos widgets podem ser adicionados como a biblioteca é estendido. No OOP, é fácil adicionar um novo tipo (para uma determinada interface): você só precisa adicionar uma nova classe e implementar todos os seus métodos (alteração de código local). Por outro lado, adicionar uma nova operação (método) a uma interface pode exigir a alteração de todas as classes que implementam essa interface (mesmo que a herança possa reduzir a quantidade de trabalho).O FP organiza o código em torno das operações (funções): cada função implementa alguma operação que pode tratar diferentes tipos de maneiras diferentes. Isso geralmente é alcançado despachando o tipo via correspondência de padrão ou algum outro mecanismo. Como conseqüência, o FP é mais apropriado quando o conjunto de tipos é estável e novas operações são adicionadas com mais frequência. Tomemos, por exemplo, um conjunto fixo de formatos de imagem (GIF, JPEG, etc) e alguns algoritmos que você deseja implementar. Cada algoritmo pode ser implementado por uma função que se comporta de maneira diferente de acordo com o tipo de imagem. Adicionar um novo algoritmo é fácil porque você só precisa implementar uma nova função (alteração de código local). Adicionar um novo formato (tipo) requer a modificação de todas as funções que você implementou até o momento para suportá-lo (alteração não local).
Conclusão: OOP e FP são fundamentalmente diferentes na maneira como organizam o código, e alterar um design de OOP para um design de FP envolveria alterar todo o seu código para refletir isso. Pode ser um exercício interessante, no entanto. Veja também estas notas de aula do livro do SICP citadas por mikemay, em particular os slides 13.1.5 a 13.1.10.
fonte