Programação funcional comparada ao OOP com classes

32

Ultimamente, tenho me interessado por alguns dos conceitos de programação funcional. Eu tenho usado OOP por algum tempo agora. Eu posso ver como eu criaria um aplicativo bastante complexo no OOP. Cada objeto saberia como fazer as coisas que o objeto faz. Ou qualquer coisa que a classe dos pais faça também. Então eu posso simplesmente dizer Person().speak()para fazer a pessoa falar.

Mas como faço coisas semelhantes em programação funcional? Eu vejo como as funções são itens de primeira classe. Mas essa função faz apenas uma coisa específica. Eu simplesmente teria um say()método flutuando e o chamaria com um equivalente de Person()argumento para saber que tipo de coisa está dizendo alguma coisa?

Para que eu possa ver as coisas simples, como eu faria o comparável entre OOP e objetos na programação funcional, para que eu possa modularizar e organizar minha base de código?

Para referência, minha principal experiência com OOP é Python, PHP e alguns C #. As linguagens que estou vendo que possuem recursos funcionais são Scala e Haskell. Embora eu esteja inclinado para Scala.

Exemplo básico (Python):

Animal(object):
    def say(self, what):
        print(what)

Dog(Animal):
    def say(self, what):
        super().say('dog barks: {0}'.format(what))

Cat(Animal):
    def say(self, what):
        super().say('cat meows: {0}'.format(what))

dog = Dog()
cat = Cat()
dog.say('ruff')
cat.say('purr')
skift
fonte
Scala é concebido como OOP + FP, para que você não tem que escolher
Karthik T
1
Sim, eu sei, mas também quero saber por razões intelectuais. Eu não consigo encontrar nada sobre o equivalente a objeto em linguagens funcionais. Quanto ao scala, eu ainda gostaria de saber quando / onde / como devo usar o funcional sobre oop, mas esse IMHO é outra questão.
Skift
2
"Particularmente enfatizado demais, a IMO é a noção de que não mantemos o estado.": Essa é a noção errada. Não é verdade que o FP não use estado, mas o FP lida com o estado de uma maneira diferente (por exemplo, mônadas em Haskell ou tipos exclusivos em Clean).
Giorgio
1
possível duplicata de Como organizar programas funcionais
Doc Brown
3
possível duplicação de Programação Funcional vs. OOP
Caleb

Respostas:

21

O que você realmente está perguntando aqui é como fazer o polimorfismo em linguagens funcionais, ou seja, como criar funções que se comportam de maneira diferente com base em seus argumentos.

Observe que o primeiro argumento para uma função é tipicamente equivalente ao "objeto" no OOP, mas em linguagens funcionais você geralmente deseja separar funções dos dados, portanto o "objeto" provavelmente será um valor de dados puro (imutável).

As linguagens funcionais em geral fornecem várias opções para alcançar o polimorfismo:

  • Algo como métodos múltiplos que chamam uma função diferente com base no exame dos argumentos fornecidos. Isso pode ser feito no tipo do primeiro argumento (que é efetivamente igual ao comportamento da maioria das linguagens OOP), mas também pode ser feito em outros atributos dos argumentos.
  • Estruturas de dados do tipo protótipo / objeto que contêm funções de primeira classe como membros . Assim, você pode incorporar uma função "dizer" dentro das estruturas de dados de cães e gatos. Efetivamente, você tornou o código parte dos dados.
  • Correspondência de padrões - onde a lógica de correspondência de padrões é incorporada à definição da função e garante comportamentos diferentes para parâmetros diferentes. Comum em Haskell.
  • Ramificação / condições - equivalente a cláusulas if / else no OOP. Pode não ser altamente extensível, mas ainda pode ser apropriado em muitos casos quando você tem um conjunto limitado de valores possíveis (por exemplo, a função passou um número ou uma string ou nula?)

Como exemplo, aqui está uma implementação Clojure do seu problema usando vários métodos:

;; define a multimethod, that dispatched on the ":type" keyword
(defmulti say :type)  

;; define specific methods for each possible value of :type. You can add more later
(defmethod say :cat [animal what] (println (str "Car purrs: " what)))
(defmethod say :dog [animal what] (println (str "Dog barks: " what)))
(defmethod say :default [animal what] (println (str "Unknown noise: " what)))

(say {:type :dog} "ruff")
=> Dog barks: ruff

(say {:type :ape} "ook")
=> Unknown noise: ook

Observe que esse comportamento não exige que nenhuma classe explícita seja definida: mapas regulares funcionam bem. A função de despacho (: type neste caso) pode ser qualquer função arbitrária dos argumentos.

Mikera
fonte
Não é 100% claro, mas o suficiente para ver para onde você está indo. Eu pude ver isso como o código 'animal' em um determinado arquivo. Também a parte de ramificação / condições também é boa. Eu não tinha considerado isso como a alternativa ao if / else.
Skift
11

Esta não é uma resposta direta, nem é necessariamente 100% precisa, pois não sou especialista em linguagem funcional. Mas, em ambos os casos, compartilharei com você minha experiência ...

Cerca de um ano atrás, eu estava em um barco semelhante ao seu. Eu fiz C ++ e C # e todos os meus projetos sempre foram muito pesados ​​no OOP. Eu ouvi falar sobre linguagens FP, li algumas informações on-line, folheei o livro F #, mas ainda não consegui entender como uma linguagem FP substitui o OOP ou pode ser útil em geral, pois a maioria dos exemplos que eu vi era simples demais.

Para mim, o "avanço" veio quando eu decidi aprender python. Eu baixei o python, fui para a página inicial do projeto euler e comecei a fazer um problema após o outro. O Python não é necessariamente uma linguagem FP e você certamente pode criar classes nele, mas comparado ao C ++ / Java / C #, ele possui muito mais construções FP, portanto, quando comecei a brincar com ele, tomei uma decisão consciente de não definir uma classe, a menos que eu precise absolutamente.

O que eu achei interessante no Python foi o quão fácil e natural era pegar funções e "costurá-las" para criar funções mais complexas e, no final, seu problema ainda era resolvido chamando uma única função.

Você apontou que, ao codificar, deve seguir o princípio de responsabilidade única e isso é absolutamente correto. Mas apenas porque a função é responsável por uma única tarefa, não significa que ela possa fazer apenas o mínimo necessário. No FP, você ainda tem níveis de abstração. Portanto, suas funções de nível superior ainda podem fazer "uma" coisa, mas podem delegar para funções de nível inferior para implementar detalhes mais finos de como essa "única" coisa é alcançada.

A chave do FP, no entanto, é que você não tem efeitos colaterais. Contanto que você trate o aplicativo como uma simples transformação de dados com um conjunto definido de entradas e um conjunto de saídas, é possível escrever um código FP que realizaria o que você precisa. Obviamente, nem todos os aplicativos se encaixam bem nesse molde, mas, quando você começar a fazê-lo, ficará surpreso com o número de aplicativos que se encaixam. E é aqui que acho que Python, F # ou Scala brilham porque eles oferecem construções de FP, mas quando você precisa se lembrar do seu estado e "apresentar efeitos colaterais", sempre pode recorrer a técnicas de OOP verdadeiras e experimentadas.

Desde então, escrevi um monte de código python como utilitários e outros scripts auxiliares para o trabalho interno, e alguns deles foram reduzidos bastante longe, mas lembrando os princípios básicos do SOLID, a maior parte desse código ainda era muito sustentável e flexível. Assim como no OOP, sua interface é uma classe e você move as classes ao refatorar e / ou adicionar funcionalidades, no FP, você faz exatamente a mesma coisa com as funções.

Na semana passada, comecei a codificar em Java e, desde então, quase diariamente sou lembrado de que, quando estou em OOP, tenho que implementar interfaces declarando classes com métodos que substituem funções, em alguns casos eu poderia conseguir o mesmo em Python usando um uma expressão lambda simples, por exemplo, 20 a 30 linhas de código que escrevi para varrer um diretório, teria de 1 a 2 linhas em Python e nenhuma classe.

FP em si são idiomas de nível superior. No Python (desculpe, minha única experiência em FP), eu poderia reunir compreensão de lista dentro de outra compreensão de lista com lambdas e outras coisas lançadas e a coisa toda seria apenas 3-4 linhas de código. Em C ++, eu conseguia absolutamente fazer a mesma coisa, mas como o C ++ é de nível inferior, eu teria que escrever muito mais código do que 3-4 linhas e, à medida que o número de linhas aumenta, meu treinamento em SRP entra em ação e eu começava. pensando em como dividir o código em partes menores (ou seja, mais funções). Mas, no interesse da manutenção e da ocultação de detalhes de implementação, eu colocaria todas essas funções na mesma classe e as tornaria privadas. E aí está ... Acabei de criar uma classe enquanto em python eu teria escrito "return (.... lambda x: .. ....)"

DXM
fonte
Sim, ele não responde diretamente à pergunta, mas ainda é uma ótima resposta. Quando escrevo scripts ou pacotes menores em python, nem sempre uso classes. muitas vezes, apenas tê-lo em um formato de pacote se encaixa perfeitamente. especialmente se eu não precisar de estado. Também concordo que a compreensão da lista também é muito útil. Desde a leitura do FP, percebi o quanto eles podem ser mais poderosos. o que me levou a querer aprender mais sobre FP, em comparação com OOP.
Skift:
Ótima resposta. Fala a toda a gente de pé ao lado da piscina funcctional, mas não tenho certeza se mergulhar seu dedo do pé na água
Robben_Ford_Fan_boy
E Ruby ... Uma de suas filosofias de design centra-se em métodos que tomam um bloco de código como argumento, normalmente um opcional. E, dada a sintaxe limpa, é fácil pensar e codificar dessa maneira. É difícil pensar e compor assim em C #. O código C # funcionalmente é detalhado e desorientador, e parece ser usado na linguagem. Eu gosto que Ruby ajudou a pensar funcionalmente mais fácil, a ver potencial na minha caixa de pensamento c #. Na análise final, vejo funcional e OO como complementar; Eu diria que Ruby certamente pensa assim.
Radarbob
8

Em Haskell, o mais próximo que você tem é "classe". Essa classe, embora não seja a mesma que a Java e C ++ , funcionará para o que você deseja neste caso.

No seu caso, é assim que o seu código será exibido.

classe Animal um onde 
dizer :: String -> som 

Então você pode ter tipos de dados individuais adaptando esses métodos.

instância Animal Dog onde
diga s = "casca" ++ s 

EDIT: - Antes que você possa se especializar, diga para Dog, você precisa informar ao sistema que Dog é animal.

Dados Dog = \ - algo aqui - \ (derivando Animal)

EDIT: - Para Wilq.
Agora, se você quiser usar say em uma função say foo, precisará informar ao haskell que foo só pode funcionar com Animal.

foo :: (Animal a) => a -> String -> String
foo a str = diga um str 

Agora, se você chamar foo com cachorro, latirá, se você chamar com gato, miará.

main = do 
let d = dog (\ - parâmetros cstr - \)
    c = gato  
no programa $ foo d "Olá Mundo"

Agora você não pode ter nenhuma outra definição de função, digamos. Se o say for chamado com algo que não seja animal, ele causará erro de compilação.

Manoj R
fonte
vou ter que usar um pouco mais de haskell para entender isso completamente, mas acho que entendi bem. Ainda estou curioso para saber como isso se alinharia com uma base de código mais complexa.
skift
nitpick O animal deve estar em maiúscula #
Daniel Gratzer
1
Como a função say sabe que você a chama em um Dog, se ela precisa apenas de uma String? E "derivar" não é apenas para algumas classes internas?
WilQu
6

Linguagens funcionais usam 2 construções para obter polimorfismo:

  • Funções de primeira ordem
  • Genéricos

Criar código polimórfico com eles é completamente diferente de como o OOP usa métodos virtuais e de herança. Embora ambos estejam disponíveis no seu idioma favorito de POO (como C #), a maioria dos idiomas funcionais (como Haskell) aumenta para onze. É raro funcionar como não genérico e a maioria das funções tem funções como parâmetros.

É difícil explicar dessa maneira e exigirá muito tempo para aprender dessa nova maneira. Mas para fazer isso, você precisa esquecer completamente o POO, porque não é assim que funciona no mundo funcional.

Eufórico
fonte
2
OOP é tudo sobre polimorfismo. Se você acha que OOP é sobre ter funções vinculadas aos seus dados, não sabe nada sobre OOP.
eufórico
4
o polimorfismo é apenas um aspecto do POO, e acho que não é o que o OP está realmente perguntando.
Doc Brown
2
O polimorfismo é o principal aspecto da POO. Tudo o resto existe para apoiá-lo. OOP sem herança / métodos virtuais é quase exatamente o mesmo que programação procedural.
eufórico
1
@ErikReppen Se "aplicar uma interface" não for necessário com frequência, você não fará OOP. Além disso, Haskell também possui módulos.
eufórico
1
Você nem sempre precisa de uma interface. Mas eles são muito úteis quando você precisa deles. E a OMI é outra parte importante do POO. Quanto aos módulos em Haskell, acho que isso provavelmente é o mais próximo do OOP para linguagens funcionais, no que diz respeito à organização do código. Pelo menos pelo que li até agora. Eu sei que eles ainda são muito diferentes.
Skift
0

realmente depende do que você deseja realizar.

se você só precisa de uma maneira de organizar o comportamento com base em critérios seletivos, pode usar, por exemplo, um dicionário (tabela de hash) com objetos de função. em python, pode ser algo como:

def bark(what):
    print "barks: {0}".format(what) 

def meow(what):
    print "meows: {0}".format(what)

def climb(how):
    print "climbs: {0}".format(how)

if __name__ == "__main__":
    animals = {'dog': {'say': bark},
               'cat': {'say': meow,
                       'climb': climb}}
    animals['dog']['say']("ruff")
    animals['cat']['say']("purr")
    animals['cat']['climb']("well")

note, no entanto, que (a) não existem 'instâncias' de cães ou gatos e (b) você terá que acompanhar o 'tipo' de seus objetos.

Como por exemplo: pets = [['martin','dog','grrrh'], ['martha', 'cat', 'zzzz']]. então você poderia fazer uma compreensão da lista como[animals[pet[1]]['say'](pet[2]) for pet in pets]

kr1
fonte
0

Às vezes, os idiomas OO podem ser usados ​​no lugar dos idiomas de baixo nível para interagir diretamente com uma máquina. C ++ Com certeza, mas mesmo para C # existem adaptadores e afins. Embora escrever código para controlar peças mecânicas e ter controle minucioso da memória seja mantido o mais próximo possível do nível mais baixo possível. Mas se esta pergunta está relacionada ao software orientado a objetos atual, como linha de negócios, aplicativos da Web, IOT, serviços da Web e a maioria dos aplicativos usados ​​em massa, então ...

Resposta, se aplicável

Os leitores podem tentar trabalhar com uma Arquitetura Orientada a Serviços (SOA). Ou seja, DDD, N-Camada, N-Camada, Hexagonal, qualquer que seja. Não vi um aplicativo de negócios grande usar eficientemente o OO "tradicional" (Active-Record ou Rich-Models), conforme descrito nas décadas de 70 e 80 na última década. (Ver nota 1)

A falha não está no OP, mas há alguns problemas com a pergunta.

  1. O exemplo que você fornece é simplesmente demonstrar polimorfismo, não é um código de produção. Às vezes, exemplos exatamente assim são tomados literalmente.

  2. No FP e SOA, os dados são separados da lógica de negócios. Ou seja, Data e Logic não andam juntos. A lógica entra em Serviços e os Dados (Modelos de Domínio) não têm comportamento polimórfico (consulte a Nota 2).

  3. Serviços e funções podem ser polimórficos. No FP, você passa funções como parâmetros para outras funções, em vez de valores. Você pode fazer o mesmo nos idiomas OO com tipos como Callable ou Func, mas ele não funciona de forma desenfreada (consulte a Nota 3). No FP e SOA, seus modelos não são polimórficos, apenas seus serviços / funções. (Ver nota 4)

  4. Há um caso de codificação incorreta nesse exemplo. Não estou falando apenas da corda vermelha "latidos de cachorro". Também estou falando sobre o próprio CatModel e DogModel. O que acontece quando você deseja adicionar uma ovelha? Você precisa entrar no seu código e criar um novo código? Por quê? No código de produção, eu preferiria ver apenas um AnimalModel com suas propriedades. Na pior das hipóteses, um AmphibianModel e um FowlModel se suas propriedades e manipulação forem muito diferentes.

Isso é o que eu esperaria ver no atual idioma "OO":

public class Animal
{
    public int AnimalID { get; set; }
    public int LegCount { get; set; }
    public string Name { get; set; }
    public string WhatISay { get; set; }
}

public class AnimalService : IManageAnimals
{
    private IPersistAnimals _animalRepo;
    public AnimalService(IPersistAnimals animalRepo) { _animalRepo = animalRepo; }

    public List<Animal> GetAnimals() => _animalRepo.GetAnimals();

    public string WhatDoISay(Animal animal)
    {
        if (!string.IsNullOrWhiteSpace(animal.WhatISay))
            return animal.WhatISay;

        return _animalRepo.GetAnimalNoise(animal.AnimalID);
    }
}

Fluxo básico

Como você muda de Classes em OO para Programação Funcional? Como outros já disseram; Você pode, mas na verdade não. O objetivo do exposto acima é demonstrar que você nem deveria usar Classes (no sentido tradicional do mundo) ao executar Java e C #. Depois de escrever o código em uma arquitetura orientada a serviços (DDD, em camadas, em camadas, hexagonal, qualquer que seja), você estará um passo mais perto do Functional porque separa seus dados (modelos de domínio) das funções lógicas (serviços).

OO Language um passo mais perto do FP

Você pode ir um pouco mais longe e dividir seus serviços SOA em dois tipos.

Classe opcional tipo 1 : Serviços comuns de implementação de interface para pontos de entrada. Estes seriam pontos de entrada "impuros" que podem chamar outras funcionalidades "Puras" ou "Impuras". Podem ser seus pontos de entrada de uma API RESTful.

Classe opcional tipo 2 : serviços puros de lógica comercial. Essas são classes estáticas que possuem a funcionalidade "pura". No FP, "Puro" significa que não há efeitos colaterais. Ele não define explicitamente Estado ou Persistência em lugar algum. (Ver nota 5)

Portanto, quando você pensa em Classes em Linguagens Orientadas a Objetos, sendo usadas em uma Arquitetura Orientada a Serviços, ele não apenas beneficia seu Código OO, mas também começa a tornar a Programação Funcional muito fácil de entender.

Notas

Nota 1 : O design orientado a objeto "Rich" ou "Active-Record" original ainda existe. Há MUITO código legado assim quando as pessoas estavam "fazendo o certo" há uma década ou mais. A última vez que vi esse tipo de código (feito corretamente) era de um videogame Codebase em C ++, onde eles controlavam a memória com precisão e tinham um espaço muito limitado. Para não dizer FP e arquiteturas orientadas a serviços são bestas e não devem considerar hardware. Mas eles priorizam a capacidade de mudar constantemente, manter, ter tamanhos de dados variáveis ​​e outros aspectos. Nos videogames e na IA da máquina, você controla os sinais e dados com muita precisão.

Nota 2 : Os modelos de domínio não possuem comportamento polimórfico nem dependências externas. Eles são "isolados". Isso não significa que eles tenham que ser 100% anêmicos. Eles podem ter muita lógica relacionada à sua construção e alteração de propriedade mutável, se aplicável. Veja DDD "Objetos de Valor" e Entidades de Eric Evans e Mark Seemann.

Nota 3 : Linq e Lambda são muito comuns. Mas quando um usuário cria uma nova função, ele raramente usa Func ou Callable como parâmetros, enquanto no FP seria estranho ver um aplicativo sem funções seguindo esse padrão.

Nota 4 : Não confunda Polimorfismo com Herança. Um CatModel pode herdar o AnimalBase para determinar quais propriedades um animal normalmente possui. Mas, como mostro, modelos como este são um cheiro de código . Se você vir esse padrão, considere dividi-lo e transformá-lo em dados.

Nota 5 : Funções puras podem (e fazem) aceitar funções como parâmetros. A função de entrada pode ser impura, mas pode ser pura. Para fins de teste, sempre seria puro. Mas na produção, embora seja tratado como puro, pode conter efeitos colaterais. Isso não muda o fato de que a função pura é pura. Embora a função de parâmetro possa ser impura. Não é confuso! : D

Suamere
fonte
-2

Você poderia fazer algo assim .. php

    function say($whostosay)
    {
        if($whostosay == 'cat')
        {
             return 'purr';
        }elseif($whostosay == 'dog'){
             return 'bark';
        }else{
             //do something with errors....
        }
     }

     function speak($whostosay)
     {
          return $whostosay .'\'s '.say($whostosay);
     }
     echo speak('cat');
     >>>cat's purr
     echo speak('dog');
     >>>dogs's bark
Michael Dennis
fonte
1
Eu não dei nenhum voto negativo. Mas acho que é porque essa abordagem não é funcional nem orientada a objetos.
Dilsh R
1
Mas o conceito transmitido é próximo do padrão utilizado na programação funcional, ou seja, $whostosaytorna-se o tipo de objeto que determina o que é executado. O acima pode ser modificado para aceitar adicionalmente outro parâmetro, $whattosaypara que um tipo que o suporte (por exemplo 'human') possa utilizá-lo.
syockit