Como refatorar um programa OO para um funcional?

26

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.

Asik
fonte
6
Eu não acho que isso é possível. você precisa reprojetar (e reescrever) tudo novamente.
Bryan Chen #
18
-1, este post é enviesado pela suposição errada de que OOP e estilo funcional são contrários. Esses são principalmente conceitos ortogonais, e IMHO é um mito de que não são. "Funcional" é mais oposto a "Procedimental", e ambos os estilos podem ser usados ​​em conjunto com OOP.
Doc Brown
11
@DocBrown, OOP depende muito de um estado mutável. Objetos sem estado não se encaixam bem na prática atual de design de POO.
SK-logic
9
@ SK-logic: a chave não são objetos sem estado, mas objetos imutáveis. E mesmo quando os objetos são mutáveis, eles geralmente podem ser usados ​​em uma parte funcional do sistema, desde que não sejam alterados dentro do contexto especificado. Além disso, acho que você sabe que objetos e fechamentos são intercambiáveis. Portanto, tudo isso mostra que OOP e "funcional" não são contrários.
Doc Brown
12
@DocBrown: Eu acho que as construções da linguagem são ortogonais, enquanto as mentalidades tendem a colidir. OOP pessoas tendem a perguntar "quais são os objetos e como eles colaboram?"; as pessoas funcionais tendem a perguntar "quais são meus dados e como desejo transformá-los?". Essas não são as mesmas perguntas e levam a respostas diferentes. Eu também acho que você interpretou mal a pergunta. Não é "OOP baba e regras de FP, como me livrar do OOP?", É "Eu obtenho OOP e não obtenho FP, existe uma maneira de transformar um programa OOP em um funcional, para que eu possa obter alguma visão? ".
Michael Shaw

Respostas:

31

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 .

GlenPeterson
fonte
Agradeço o esforço, a organização e a comunicação clara nesta resposta; mas eu tenho que ter um pequeno problema com alguns dos técnicos. Uma das teclas mencionadas na parte superior é a composição de funções; isso remonta ao porquê funções em grande parte encapsuladoras dentro de objetos não produzem um propósito: se uma função está dentro de um objeto, ela deve estar lá para agir sobre esse objeto; e se ele age sobre esse objeto, deve estar mudando de interior. Agora eu vou perdoá que nem todos requer transparência ou imutabilidade referencial, mas se ele muda o objeto no local já não precisa devolvê-lo
Jimmy Hoffa
E assim que uma função não retorna um valor, de repente a função não pode ser composta por outras e você perde toda a abstração da composição funcional. Você pode fazer com que a função altere o objeto no lugar e depois retorne o objeto, mas se estiver fazendo isso, por que não fazer com que a função tome o objeto como parâmetro e liberte-o dos limites do objeto pai? Liberado do objeto pai, ele também poderá trabalhar em outros tipos, o que é outra parte importante do FP que está faltando: Tipo abstração. seu forEachPasenger só funciona contra os passageiros ...
Jimmy Hoffa
1
O motivo pelo qual você abstrai coisas para mapear e reduzir, e essas funções não estão vinculadas a conter objetos é para que possam ser usadas em uma infinidade de tipos por meio de polimorfismo paramétrico. É a conflagração dessas abstrações variadas que você não encontra nas linguagens OOP que realmente define o FP e o leva a ter valor. Não é que a preguiça, a transparência referencial, imutabilidade, ou até mesmo o sistema de tipos HM são necessárias para criar FP, essas coisas são efeitos em vez colaterais de criar linguagens purposed para a composição funcional, onde as funções podem abstrato sobre tipos geralmente
Jimmy Hoffa
@ JimmyHoffa Você fez uma crítica muito justa ao meu exemplo. Fui seduzido pela mutabilidade pela interface do consumidor Java8. Além disso, a definição de FP de chouser / fogus não incluía imutabilidade e eu adicionei a definição de Odersky / Spoon / Venners posteriormente. Deixei o exemplo original, mas adicionei uma versão nova e imutável em uma seção "PS" na parte inferior. É feio. Mas acho que demonstra funções agindo sobre objetos para produzir novos objetos, em vez de alterar as partes internas dos originais. Ótimo comentário!
GlenPeterson
1
Esta conversa continua no quadro branco: chat.stackexchange.com/transcript/message/11702383#11702383
GlenPeterson
12

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:

  • Converta todo o estado externo em um parâmetro da função. EG: se o método de um objeto for modificado x, faça com que o método seja passado em xvez de chamar this.x.
  • Remova o comportamento dos objetos.
    1. Tornar os dados do objeto acessíveis ao público
    2. Converta todos os métodos em funções que o objeto chama.
    3. O código do cliente que chama o objeto chama a nova função passando os dados do objeto. EG: converter x.methodThatModifiesTheFooVar()emfooFn(x.foo)
    4. Remova o método original do objeto
  • Substituir como muitos laços iterativos como você pode com maior funções de ordem como 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.

Daniel Kaplan
fonte
+1 Dependendo do que exatamente você está tentando fazer, provavelmente é o máximo que você pode fazer sem fazer alterações no design que iriam além da "refatoração".
Evicatos
@ Evicatos: Não sei, se o JavaScript tivesse melhor suporte para o estado imutável, acho que minha solução seria tão funcional quanto você obteria em uma linguagem funcional dinâmica como o Clojure. O que é um exemplo de algo que exigiria algo além da refatoração?
Daniel Kaplan
Eu acho que se livrar do estado mutável se qualificaria. Não acho que seja apenas uma questão de melhor suporte na linguagem, acho que passar de mutável para imutável basicamente sempre exige mudanças arquitetônicas fundamentais que constituem essencialmente uma reescrita. Ymmv, dependendo da sua definição de refatoração.
Evicatos
@Evicatos see my edit
Daniel Kaplan
1
@tieTYT sim, isso é triste sobre JS ser tão mutável, mas pelo menos Clojure pode compilar para JavaScript: github.com/clojure/clojurescript
GlenPeterson
3

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.

Uri
fonte
Eu acrescentaria que uma boa primeira marca é buscar a transparência referencial. Depois disso, você obtém ~ 50% dos benefícios da programação funcional.
Daniel Gratzer #
3

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)

Marcus
fonte
2

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.

Giorgio
fonte