Quais recursos funcionais valem um pouco de confusão de OOP pelos benefícios que trazem?

13

Depois de aprender a programação funcional em Haskell e F #, o paradigma OOP parece invertido com classes, interfaces, objetos. Quais aspectos do PF posso trazer para o trabalho que meus colegas de trabalho possam entender? Vale a pena conversar com meu chefe sobre estilos de reciclagem da minha equipe para que possamos usá-los?

Possíveis aspectos do PF:

  • Imutabilidade
  • Aplicação Parcial e Caril
  • Funções de Primeira Classe (ponteiros de função / Objetos Funcionais / Padrão de Estratégia)
  • Avaliação preguiçosa (e mônadas)
  • Funções puras (sem efeitos colaterais)
  • Expressões (vs. declarações - cada linha de código produz um valor em vez de ou além de causar efeitos colaterais)
  • Recursão
  • Correspondência de padrões

É um espaço livre para todos, onde podemos fazer o que a linguagem de programação suportar até o limite que a linguagem suporta? Ou existe uma orientação melhor?

Trident D'Gao
fonte
6
Eu tive uma experiência parecida. Após cerca de 2 meses de dor, comecei a encontrar um bom equilíbrio entre "coisas que mapeiam objetos" e "coisas que mapeiam funções". Ajuda a fazer alguns hackers sérios em um idioma que suporta ambos. No final, ambos os meus FP e OOP habilidades melhoraram muito
Daniel Gratzer
3
FWIW, o Linq é funcional e lento, e você pode simular a programação funcional em C # usando métodos estáticos e evitando a persistência de estado.
9788 Robert Harvey
1
Neste ponto, você deve ler sicp . É gratuito e bem escrito. Ele oferece uma boa comparação entre os dois paradigmas.
Simon Bergot
4
FP e POO são ao mesmo tempo em algum sentido ortogonais e em algum sentido duplos. OOP é sobre abstração de dados, FP é sobre (a ausência de) efeitos colaterais. Se você tem ou não efeitos colaterais, é ortogonal à forma como você abstrai seus dados. O Lambda Calculus é funcional e orientado a objetos, por exemplo. Sim, o FP normalmente usa tipos de dados abstratos, não objetos, mas você também pode usar objetos sem ser menos FP. OTOH, há também uma relação profunda: a função é isomorfo a um objeto com apenas um método (que é como eles são "falsas" em Java, e implementadas em Java8, ...
Jörg W Mittag
3
Acho que o aspecto mais forte da sua pergunta tem a ver com legibilidade. "Quanto estilo de Programação Funcional é apropriado para levar ao trabalho em uma loja Orientada a Objetos?" Ou quais recursos funcionais valem um pouco de confusão de OOP pelos benefícios que trazem.
GlenPeterson

Respostas:

13

A programação funcional é um paradigma diferente da programação orientada a objetos (uma mentalidade diferente e uma maneira diferente de pensar sobre os programas). Você começou a perceber que aqui há mais de uma maneira (orientada a objetos) para pensar sobre os problemas e suas soluções. Existem outros (programação processual e genérica vêm à mente). Como você reage a esse novo conhecimento, se você aceita e integra essas novas ferramentas e abordagens ao seu conjunto de habilidades, determinará se você cresce e se torna um desenvolvedor mais completo e qualificado.

Todos somos treinados para lidar com isso e nos sentimos confortáveis ​​com um certo nível de complexidade. Eu gosto de chamar isso de limite hrair de uma pessoa (de Watership Down, quão alto você pode contar). É ótimo expandir sua mente, sua capacidade de considerar mais opções e ter mais ferramentas para abordar e resolver problemas. Mas é uma mudança e tira você da sua zona de conforto.

Um problema que você pode encontrar é que ficará menos conteúdo para acompanhar a multidão "tudo é um objeto". Talvez você precise desenvolver paciência ao trabalhar com pessoas que podem não entender (ou querer entender) por que uma abordagem funcional para o desenvolvimento de software funciona bem para certos problemas. Assim como uma abordagem de programação genérica funciona bem para certos problemas.

Boa sorte!

ChuckCottrill
fonte
3
Gostaria de acrescentar que é possível entender melhor alguns conceitos tradicionais de OOP ao trabalhar com linguagens funcionais como Haskell ou Clojure. Pessoalmente, percebi como o polimorfismo é realmente um conceito importante (interfaces em Java ou classes de letras em Haskell), enquanto a herança (o que eu pensava ser um conceito definidor) é uma abstração estranha.
Wirrbel #
6

A programação funcional gera produtividade muito prática e prática na escrita diária de códigos: alguns recursos favorecem a dispersão, o que é ótimo porque quanto menos código você escreve, menos falhas você faz e menos manutenção é necessária.

Sendo um matemático, acho as coisas funcionais sofisticadas muito atraentes, mas geralmente é útil ao projetar um aplicativo: essas estruturas podem codificar na estrutura do programa muitos invariantes do programa, sem representar esses invariantes por variáveis.

Minha combinação favorita pode parecer bastante trivial, no entanto, acredito que ela tenha um impacto muito alto na produtividade. Essa combinação é de Aplicação Parcial e Funções de Currying e Primeira Classe, que eu rotularia de novo para nunca mais escrever um loop for : em vez disso, passe o corpo do loop para uma função de iteração ou mapeamento. Recentemente, fui contratado para um trabalho em C ++ e notei divertidamente que perdi o hábito de escrever loops!

A combinação de recursão e correspondência de padrões aniquila a necessidade desse padrão de design do visitante . Basta comparar o código que você precisa para programar um avaliador de expressões booleanas: em qualquer linguagem de programação funcional, isso deve ter cerca de 15 linhas de código; em OOP, a coisa certa a fazer é usar esse padrão de design do Visitor , que transforma esse exemplo de brinquedo em um extenso ensaio. As vantagens são óbvias e não conheço nenhum inconveniente.

user40989
fonte
2
Eu concordo completamente, mas recebi respostas de pessoas que, em toda a indústria, tendem a concordar: elas sabem o padrão de visitantes, viram e o usaram muitas vezes; portanto, o código nele é algo que eles entendem e estão familiarizados, o outra abordagem, embora ridiculamente mais simples e fácil, é estranha e, portanto, mais difícil para eles. Este é um fato infeliz da indústria ter tido mais de 15 anos de OOP em cada cabeça de programador que mais de 100 linhas de código são mais fáceis de entender do que 10, simplesmente porque memorizaram essas mais de 100 linhas após repeti-las por mais de uma década
Jimmy Hoffa
1
-1 - Um código mais conciso não significa que você está escrevendo um código "menos". Você está escrevendo o mesmo código usando menos caracteres. Se houver alguma coisa, você cometerá mais erros porque o código é (geralmente) mais difícil de ler.
Telastyn #
8
@Telastyn: Terse não é o mesmo que ilegível. Além disso, grandes massas de chapas incandescentes têm sua própria maneira de ser ilegível.
Michael Shaw
1
@ Telastyn Eu acho que você acabou de tocar no ponto crucial aqui, sim, o concerto pode ser ruim e ilegível, inchado também pode ser ruim e ilegível, mas a chave não é o tamanho da variável e o código puzzlicamente escrito. A chave é como você menciona acima número de operações, eu discordo que o número de operações não se correlaciona a manutenção, eu acho que fazer menos coisas (com código escrito claramente) faz a leitura benefício e manutenção. Obviamente, fazendo o mesmo número de coisas com a função única letra e variável nomes não vai ajudar, boa FP precisa consideravelmente menos operações ainda claramente escritos
Jimmy Hoffa
2
@ user949300: se você quer um exemplo completamente diferente, que tal este exemplo do Java 8 ?: list.forEach(System.out::println);Do ponto de vista do FP, printlné uma função que aceita dois argumentos, um destino PrintStreame um valor, Objectmas o método do Collections forEachespera uma função com apenas um argumento que pode ser aplicado a todos os elementos. Portanto, o primeiro argumento está vinculado à instância encontrada ao System.outceder a uma nova função com um argumento. É mais simples queBiConsumer<…> c=PrintStream::println; PrintStream a1=System.out; list.forEach(a2 -> c.accept(a1, a2));
Holger
5

Você pode ter que restringir quais partes do seu conhecimento você usa no trabalho, da maneira que o Super-homem tem que fingir ser Clark Kent para aproveitar as vantagens de uma vida normal. Mas saber mais nunca vai te machucar. Dito isto, alguns aspectos da Programação Funcional são apropriados para uma loja Orientada a Objetos, e outros podem valer a pena conversar com seu chefe, para que você possa aumentar o nível médio de conhecimento da sua loja e escrever um código melhor como resultado.

FP e OOP não são mutuamente exclusivos. Olhe para Scala. Alguns acham que é o pior porque é um FP impuro, mas alguns acham que é o melhor pelo mesmo motivo.

Um por um, aqui estão alguns aspectos que funcionam muito bem com OOP:

  • Funções puras (sem efeitos colaterais) - Todas as linguagens de programação que conheço suportam isso. Eles tornam seu código muito, muito mais fácil de raciocinar e devem ser usados ​​sempre que possível. Você não precisa chamá-lo de FP. Apenas chame de boas práticas de codificação.

  • Imutabilidade: String é sem dúvida o objeto Java mais usado e é imutável. Eu cubro objetos imutáveis em Java e imutáveis coleções de Java no meu blog. Parte disso pode ser aplicável a você.

  • Funções de primeira classe (ponteiros de função / objetos funcionais / padrão de estratégia) - O Java possui uma versão mutante e prejudicada desde a versão 1.1, com a maioria das classes de API (e existem centenas) que implementam a interface do Listener. Runnable é provavelmente o objeto funcional mais comumente usado. As funções de primeira classe são mais trabalhosas para codificar em um idioma que não as suporta nativamente, mas às vezes valem o esforço extra quando simplificam outros aspectos do seu código.

  • A recursão é útil para processar árvores. Em uma loja OOP, esse é provavelmente o principal uso apropriado de recursão. O uso de recursão por diversão no OOP provavelmente deve ser desaprovado, se por nenhum outro motivo, a maioria dos idiomas OOP não tiver o espaço de pilha por padrão para tornar essa uma boa idéia.

  • Expressões (vs. Declarações - cada linha de código produz um valor em vez de, ou além de causar efeitos colaterais) - O único operador avaliador em C, C ++ e Java é o Operador Ternário . Discuto o uso apropriado no meu blog. Você pode escrever algumas funções simples que são altamente reutilizáveis ​​e avaliativas.

  • Avaliação Preguiçosa (e Mônadas) - principalmente restrita à Inicialização preguiçosa no OOP. Sem os recursos de idioma para suportá-lo, você pode encontrar algumas APIs que são úteis, mas escrever sua própria é difícil. Maximize o uso de fluxos - consulte as interfaces Writer e Reader para obter exemplos.

  • Aplicação Parcial e Caril - Não é prático sem as funções de primeira classe.

  • Correspondência de padrões - geralmente desencorajada em OOP.

Em resumo, eu não acho que o trabalho deva ser gratuito para todos, onde você pode fazer o que a linguagem de programação suportar, até o limite que ela suporta. Acho que a legibilidade de seus colegas de trabalho deve ser seu teste decisivo para códigos feitos para contratação. Onde isso o incomoda mais, eu gostaria de começar uma educação no trabalho para ampliar os horizontes de seus colegas de trabalho.

GlenPeterson
fonte
Desde que aprendi FP, eu costumava projetar coisas para ter interfaces fluentes, o que resulta no que é semelhante a expressões, uma função que possui uma declaração que faz várias coisas. É o mais próximo que você realmente vai chegar, mas é uma abordagem que flui naturalmente da pureza quando você descobre que não possui mais nenhum método nulo, usando métodos de extensão estática em C # ajuda muito. Dessa forma o seu ponto de expressão é o único ponto eu discordo, tudo o resto está no local com as minhas próprias experiências de aprendizagem FP e trabalhar um dia de trabalho .NET
Jimmy Hoffa
O que realmente me incomoda no C # agora é que não posso usar delegados em vez de interfaces de método único por dois motivos simples: 1. você não pode criar lambas recursivas sem um hack (atribuindo primeiro a null e depois a um segundo lambda) ou Y- combinador (que é tão feio como o inferno em C #). 2. não há aliases de tipo que você possa usar no escopo do projeto; portanto, as assinaturas de seus delegados rapidamente se tornam incontroláveis. Portanto, apenas por esses dois motivos estúpidos, não posso mais aproveitar o C #, porque a única coisa que posso fazer funcionar é usar interfaces de método único, que são apenas um trabalho extra desnecessário.
Trident D'Gao
@bonomo O Java 8 possui um java.util.function.BiConsumer genérico que pode ser útil em C #: public interface BiConsumer<T, U> { public void accept(T t, U u); }Existem outras interfaces funcionais úteis no java.util.function.
GlenPeterson
@bonomo Ei, entendi, essa é a dor de Haskell. Sempre que você lê alguém diz "Aprender FP me fez melhorar na OOP" significa que eles aprenderam Ruby ou algo que não é puro e declarativo como Haskell. Haskell deixa claro que OOP é um nível inutilmente baixo. A maior dor que você encontra é que a inferência de tipo baseada em restrições é indecidível quando você não está no sistema de tipos HM, portanto, a inferência de tipo baseada em cosnagens não é completamente feita: blogs.msdn.com/b/ericlippert/archive / 2012/03/09 /…
Jimmy Hoffa
1
Most OOP languages don't have the stack space for it Verdade? Tudo o que você precisa é de 30 níveis de recursão para gerenciar bilhões de nós em uma árvore binária equilibrada. Tenho certeza de que meu espaço de pilha é adequado para muito mais níveis do que isso.
9788 Robert
3

Além da programação funcional e da programação orientada a objetos, também há programação declarativa (SQL, XQuery). Aprender cada estilo ajuda a obter novas idéias e você aprenderá a escolher a ferramenta certa para o trabalho.

Mas sim, pode ser muito frustrante escrever código em um idioma e saber que se você estivesse usando outra coisa, seria muito mais produtivo para um domínio de problema específico. No entanto, mesmo se você estiver usando uma linguagem como Java, é possível aplicar conceitos do FP ao seu código Java, ainda que de maneira indireta. A estrutura do Guava, por exemplo, faz parte disso.

sgwizdak
fonte
2

Como programador, acho que você nunca deve parar de aprender. Dito isto, é muito interessante que o aprendizado de FP esteja manchando suas habilidades de OOP. Costumo pensar em aprender POO como aprender a andar de bicicleta; você nunca esquece como fazê-lo.

À medida que aprendi os meandros do FP, me vi pensando mais matematicamente e ganhei uma melhor perspectiva dos meios pelos quais escrevo software. Essa é a minha experiência pessoal.

À medida que você ganha mais experiência, os principais conceitos de programação serão muito mais difíceis de perder. Então, sugiro que você relaxe no FP até que os conceitos de POO sejam totalmente solidificados em sua mente. FP é uma mudança de paradigma definitiva. Boa sorte!

Bobby Gammill
fonte
4
Aprender OOP é como aprender a rastrear. Mas uma vez que você se levante com firmeza, só recorrerá a engatinhar quando estiver bêbado demais. Claro que você não pode esquecer como fazê-lo, mas normalmente não gostaria. E será uma experiência dolorosa andar com os rastreadores quando você souber que pode correr.
SK-logic
@ SK-lógica, eu gosto do seu methaphor
Trident D'Gao
@ SK-Logic: Como é o aprendizado de programação imperativa? Arrastando-se de bruços?
22813 Robert Harvey
@RobertHarvey tentando escavar no subsolo com uma colher enferrujada e um baralho de cartões perfurados.
Jimmy Hoffa
0

Já existem muitas respostas boas, portanto as minhas abordarão um subconjunto da sua pergunta; ou seja, eu me ofendo à premissa de sua pergunta, já que OOP e recursos funcionais não são mutuamente exclusivos.

Se você usa C ++ 11, existem muitos desses tipos de recursos de programação funcional incorporados à biblioteca de idiomas / padrão que sinergizam (muito) bem com o OOP. Obviamente, não tenho certeza de quão bem o TMP será recebido por seu chefe ou colegas de trabalho, mas o ponto é que você pode obter muitos desses recursos de uma forma ou de outra em linguagens / OOP não funcionais, como C ++.

O uso de modelos com recursão no tempo de compilação depende dos seus três primeiros pontos,

  • Imutabilidade
  • Recursão
  • Correspondência de padrões

Como os valores do modelo são imutáveis ​​(constantes em tempo de compilação), qualquer iteração é feita usando recursão e a ramificação é feita usando (mais ou menos) correspondência de padrões, na forma de resolução de sobrecarga.

Quanto aos outros pontos, o uso std::binde std::functiona aplicação parcial de funções são fornecidas, e os ponteiros de função estão embutidos no idioma. Objetos que podem ser chamados são objetos funcionais (assim como aplicativos de funções parciais). Observe que, por objetos que podem ser chamados, quero dizer aqueles que definem seus operator ().

Avaliação preguiçosa e funções puras seriam um pouco mais difíceis; para funções puras, você pode usar funções lambda que capturam apenas por valor, mas isso não é o ideal.

Por fim, aqui está um exemplo de uso de recursão em tempo de compilação com aplicativo de função parcial. É um exemplo um tanto artificial, mas demonstra a maioria dos pontos acima. Ele vincula recursivamente os valores de uma tupla a uma determinada função e gera um objeto de função (que pode ser chamado)

#include <iostream>
#include <functional>

//holds a compile-time index sequence
template<std::size_t ... >
struct index_seq
{};

//builds the index_seq<...> struct with the indices (boils down to compile-time indexing)
template<std::size_t N, std::size_t ... Seq>
struct gen_indices
  : gen_indices<N-1, N-1, Seq ... >
{};

template<std::size_t ... Seq>
struct gen_indices<0, Seq ... >
{
    typedef index_seq<Seq ... > type;
};


template <typename RType>
struct bind_to_fcn
{
    template <class Fcn, class ... Args>
    std::function<RType()> fcn_bind(Fcn fcn, std::tuple<Args...> params)
    {
        return bindFunc(typename gen_indices<sizeof...(Args)>::type(), fcn, params);
    }

    template<std::size_t ... Seq, class Fcn, class ... Args>
    std::function<RType()> bindFunc(index_seq<Seq...>, Fcn fcn, std::tuple<Args...> params)
    {
        return std::bind(fcn, std::get<Seq>(params) ...);
    }
};

//some arbitrary testing function to use
double foo(int x, float y, double z)
{
    return x + y + z;
}

int main(void)
{
    //some tuple of parameters to use in the function call
    std::tuple<int, float, double> t = std::make_tuple(1, 2.04, 0.1);                                                                                                                                                                                                      
    typedef double(*SumFcn)(int,float,double);

    bind_to_fcn<double> binder;
    auto other_fcn_obj = binder.fcn_bind<SumFcn>(foo, t);
    std::cout << other_fcn_obj() << std::endl;
}
alrikai
fonte