Por que o uso do operador de atribuição ou loops não é recomendado na programação funcional?

9

Se minha função atender abaixo de dois requisitos, acredito que a função Sum retorne a soma dos itens em uma lista onde o item é avaliado como verdadeiro para uma determinada condição qualificada para ser referida como função pura, não é?

1) Para um determinado conjunto de i / p, o mesmo o / p é retornado independentemente do tempo em que a função é chamada

2) Não tem nenhum efeito colateral

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if(predicate(item)) result += item;
    return result;
}

Exemplo: Sum(x=>x%2==0, new List<int> {1,2,3,4,5...100});

A razão pela qual estou fazendo essa pergunta é porque vejo quase todos os lugares onde as pessoas recomendam evitar o operador de atribuição e os loops, porque é um estilo de programação imperativo. Então, o que pode dar errado no exemplo acima, que utiliza loops e operador de atribuição no contexto da programação de funções?

rahulaga_dev
fonte
11
Ele não tem nenhum efeito colateral - ele tem efeito colateral, quando a itemvariável sofre mutação no loop.
Fabio
@Fabio ok. Mas você pode elaborar sobre o escopo do efeito colateral?
rahulaga_dev

Respostas:

16

O que há na programação funcional que faz a diferença?

A programação funcional é, por princípio, declarativa . Você diz qual é o seu resultado, em vez de como calculá-lo.

Vamos dar uma olhada na implementação realmente funcional do seu snippet. Em Haskell, seria:

predsum pred numbers = sum (filter pred numbers)

Está claro qual é o resultado? Bastante, é a soma dos números que atendem ao predicado. Como é calculado? Eu não ligo, pergunte ao compilador.

Você poderia dizer que usar sume filteré um truque e não conta. Vamos implementá-lo sem esses ajudantes (embora a melhor maneira seja implementá-los primeiro).

A solução "Functional Programming 101" que não usa sumé com recursão:

sum pred list = 
    case list of
        [] -> 0
        h:t -> if pred h then h + sum pred t
                         else sum pred t

Ainda está bem claro qual é o resultado em termos de chamada de função única. É 0ou recursive call + h or 0, dependendo pred h. Ainda bem direto, mesmo que o resultado final não seja imediatamente óbvio (embora com um pouco de prática isso realmente pareça um forloop).

Compare isso com a sua versão:

public int Sum(Func<int,bool> predicate, IEnumerable<int> numbers){
    int result = 0;
    foreach(var item in numbers)
        if (predicate(item)) result += item;
    return result;
}

Qual é o resultado? Oh, eu vejo: única returndeclaração, sem surpresas aqui: return result.

Mas o que é isso result? int result = 0? Não parece certo. Você faz algo mais tarde com isso 0. Ok, você adiciona items a ele. E assim por diante.

É claro que, para a maioria dos programadores, isso é bastante óbvio, o que acontece em uma função simples como essa, mas adicione uma returndeclaração extra ou algo assim e de repente fica mais difícil de rastrear. Todo o código é sobre como e o que resta para o leitor descobrir - esse é claramente um estilo muito imperativo .

Então, as variáveis ​​e os loops estão errados?

Não.

Há muitas coisas que são muito mais fáceis de serem explicadas por eles e muitos algoritmos que exigem que o estado mutável seja rápido. Porém, as variáveis ​​são inerentemente imperativas, explicando como, em vez de o quê , e dando pouca previsão de qual pode ser seu valor algumas linhas depois ou após algumas iterações de loop. Os loops geralmente exigem que o estado faça sentido e, portanto, são inerentemente imperativos.

Variáveis ​​e loops simplesmente não são programação funcional.

Sumário

A programação funcional contemporânea é um pouco mais de estilo e um modo de pensar útil do que um paradigma. A forte preferência pelas funções puras está nessa mentalidade, mas na verdade é apenas uma pequena parte.

A maioria das linguagens comuns permite que você use algumas construções funcionais. Por exemplo, em Python, você pode escolher entre:

result = 0
for num in numbers:
    if pred(result):
        result += num
return result

ou

return sum(filter(pred, numbers))

ou

return sum(n for n in numbers if pred(n))

Essas expressões funcionais são adequadas para problemas desse tipo e simplesmente tornam o código mais curto (e menor é bom ). Você não deve substituir pensativamente o código imperativo por eles, mas quando eles se encaixam, eles quase sempre são uma escolha melhor.

Frax
fonte
thx para uma boa explicação !!
Rhulaga_dev 21/10
11
@RahulAgarwal Você pode achar esta resposta interessante, mas captura bem um conceito tangencial de estabelecer verdades versus descrever as etapas. Também gosto da frase "linguagens declarativas contêm efeitos colaterais, enquanto linguagens imperativas não" - geralmente os programas funcionais apresentam cortes limpos e muito visíveis entre o código de estado que lida com o mundo exterior (ou a execução de algum algoritmo otimizado) e o código puramente funcional.
Frax
11
@Frax: obrigado !! Vou dar uma olhada. Também recentemente me deparei com Rich Hickey falar sobre o valor dos valores É realmente brilhante. Eu acho que uma regra geral - "trabalhe com valores e expressão, em vez de trabalhar com algo que mantém valor e pode mudar"
rahulaga_dev 26/10/18
11
@Frax: Também é justo dizer que o FP é uma abstração sobre a programação imperativa - porque, finalmente, alguém tem que instruir a máquina sobre "como fazer", certo? Se sim, a programação imperativa não tem um controle de nível mais baixo comparado ao FP?
rahulaga_dev
11
@ Frax: Eu concordo com Rahul que o imperativo é o nível mais baixo, no sentido de que está mais próximo da máquina subjacente. Se o hardware pudesse fazer cópias de dados sem nenhum custo, não precisaríamos de atualizações destrutivas para melhorar a eficiência. Nesse sentido, o paradigma imperativo está mais próximo do metal.
Giorgio
9

O uso do estado mutável geralmente é desencorajado na programação funcional. Os loops são desencorajados como conseqüência, porque os loops são úteis apenas em combinação com o estado mutável.

A função como um todo é pura, o que é ótimo, mas o paradigma da programação funcional não se aplica apenas ao nível de funções inteiras. Você também deseja evitar o estado mutável também no nível local, dentro das funções. E o raciocínio é basicamente o mesmo: evitar o estado mutável facilita a compreensão do código e evita certos erros.

No seu caso, você poderia escrever o numbers.Where(predicate).Sum()que é claramente muito mais simples. E mais simples significa menos erros.

JacquesB
fonte
THX !! Eu acho que estava faltando uma linha impressionante - mas o paradigma da programação funcional não se aplica apenas ao nível de funções inteiras. Mas agora também estou me perguntando como visualizar esse limite. Basicamente, da perspectiva do consumidor, é função pura, mas o desenvolvedor que realmente escreveu essa função não seguiu as diretrizes da função pura? confuso :(
rahulaga_dev 21/10
@RahulAgarwal: Que limite?
precisa saber é o seguinte
Estou confuso no sentido de que se o paradigma de programação se qualificar para ser o FP do ponto de vista do consumidor de funções? Bcoz Se eu olhar para implementação implementação de Whereno numbers.Where(predicate).Sum()- ele faz uso de foreachloop.
rahulaga_dev 21/10
3
@RahulAgarwal: Como consumidor de uma função, você realmente não se importa se uma função ou módulo usa internamente um estado mutável, desde que seja externamente puro.
precisa saber é o seguinte
7

Enquanto você está certo de que, do ponto de vista de um observador externo, sua Sumfunção é pura, a implementação interna claramente não é pura - você tem um estado armazenado no resultqual você muda repetidamente. Uma das razões para evitar estado mutável é porque produz uma maior carga cognitiva sobre o programador, que por sua vez leva a mais erros [carece de fontes?] .

Embora em um exemplo simples como esse, a quantidade de estado mutável sendo armazenada seja provavelmente pequena o suficiente para não causar problemas sérios, o princípio geral ainda se aplica. Um exemplo de brinquedo como Sumprovavelmente não é a melhor maneira de ilustrar a vantagem da programação funcional sobre o imperativo - tente fazer algo com muito estado mutável e as vantagens podem ficar mais claras.

Philip Kendall
fonte