Pourquoi utiliser l'opérateur d'affectation ou les boucles déconseillés dans la programmation fonctionnelle?

9

Si ma fonction répond à moins de deux exigences, je crois que la fonction Sum pour renvoyer la somme des éléments dans une liste où l'élément est évalué à vrai pour une condition donnée peut être appelée fonction pure, n'est-ce pas?

1) Pour un ensemble donné de i / p, le même o / p est renvoyé quel que soit le moment où la fonction est appelée

2) Il n'a aucun effet secondaire

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;
}

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

La raison pour laquelle je pose cette question est parce que je vois presque partout où les gens conseillent d'éviter les opérateurs d'affectation et les boucles car c'est un style de programmation impératif. Alors, qu'est-ce qui peut mal tourner avec l'exemple ci-dessus qui utilise des boucles et un opérateur d'affectation dans le contexte de la programmation de fonctions?

rahulaga_dev
la source
1
Il n'a pas d'effet secondaire - il a un effet secondaire, quand la itemvariable mute dans la boucle.
Fabio
@Fabio ok. Mais pouvez-vous préciser la portée des effets secondaires?
rahulaga_dev

Réponses:

16

Qu'est-ce qui fait la différence dans la programmation fonctionnelle?

La programmation fonctionnelle est par principe déclarative . Vous dites quel est votre résultat au lieu de savoir comment le calculer.

Jetons un coup d'œil à l'implémentation vraiment fonctionnelle de votre extrait. À Haskell, ce serait:

predsum pred numbers = sum (filter pred numbers)

Est - il clair que le résultat est? Tout à fait ainsi, c'est la somme des nombres rencontrant le prédicat. Comment est-il calculé? Je m'en fiche, demandez au compilateur.

Vous pourriez peut-être dire que l'utilisation de sumet filterest une astuce et cela ne compte pas. Laissez-le ensuite implémenter sans ces aides (bien que la meilleure façon serait de les implémenter en premier).

La solution "Functional Programming 101" qui n'utilise pas sumest à récursivité:

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

Il est encore assez clair quel est le résultat en termes d'appel de fonction unique. C'est soit 0, soit recursive call + h or 0, selon pred h. Toujours assez simple, même si le résultat final n'est pas immédiatement évident (bien qu'avec un peu de pratique, cela se lit vraiment comme une forboucle).

Comparez cela à votre version:

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;
}

Quel est le résultat? Oh, je vois: seule returndéclaration, pas de surprise ici: return result.

Mais c'est quoi result? int result = 0? Cela ne semble pas correct. Vous faites quelque chose plus tard avec ça 0. Ok, vous y ajoutez des items. Etc.

Bien sûr, pour la plupart des programmeurs, c'est assez évident ce qui se passe dans une fonction simple comme celle-ci, mais ajoutez une returninstruction supplémentaire ou plus et il devient soudainement plus difficile à suivre. Tout le code consiste à savoir comment et ce qu'il reste au lecteur à comprendre - c'est clairement un style très impératif .

Alors, les variables et les boucles sont-elles incorrectes?

Non.

Il y a beaucoup de choses qui sont beaucoup plus faciles à expliquer par eux, et de nombreux algorithmes qui nécessitent un état mutable pour être rapide. Mais les variables sont intrinsèquement impératives, expliquant comment au lieu de quoi , et donnant peu de prédiction de ce que leur valeur peut être quelques lignes plus tard ou après quelques itérations de boucle. Les boucles nécessitent généralement un état pour avoir un sens, et elles sont donc également intrinsèquement impératives.

Les variables et les boucles ne sont tout simplement pas une programmation fonctionnelle.

Sommaire

La programmation fonctionnellement contemporaine est un peu plus de style et une façon de penser utile qu'un paradigme. Une forte préférence pour les fonctions pures est dans cet état d'esprit, mais ce n'est qu'une petite partie en fait.

La plupart des langages répandus vous permettent d'utiliser certaines constructions fonctionnelles. Par exemple en Python, vous pouvez choisir 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))

Ces expressions fonctionnelles conviennent parfaitement à ce genre de problèmes et rendent simplement le code plus court (et plus court est bon ). Vous ne devriez pas remplacer inconsidérément le code impératif par eux, mais lorsqu'ils conviennent, ils sont presque toujours un meilleur choix.

Frax
la source
merci pour une belle explication !!
rahulaga_dev
1
@RahulAgarwal Vous pouvez trouver cette réponse intéressante, elle capture bien un concept tangentiel d'établissement de vérités par rapport à la description des étapes. J'aime aussi l'expression "les langages déclaratifs contiennent des effets secondaires alors que les langages impératifs n'en ont pas" - les programmes fonctionnels ont généralement des coupes nettes et très visibles entre le code avec état traitant du monde extérieur (ou exécutant un algorithme optimisé) et le code purement fonctionnel.
Frax
1
@Frax: merci !! Je vais y jeter un œil. Récemment, Rich Hickey a également parlé de la valeur des valeurs. C'est vraiment génial. Je pense qu'une règle de pouce - "travailler avec des valeurs et des expressions plutôt que de travailler avec quelque chose qui
contient de la
1
@Frax: Il est également juste de dire que FP est une abstraction sur la programmation impérative - parce que finalement, quelqu'un doit enseigner à la machine "comment faire", non? Si oui, alors la programmation impérative n'a-t-elle pas plus de contrôle de bas niveau par rapport à FP?
rahulaga_dev
1
@Frax: Je serais d'accord avec Rahul que l'impératif est un niveau inférieur dans le sens où il est plus proche de la machine sous-jacente. Si le matériel pouvait faire des copies de données sans frais, nous n'aurions pas besoin de mises à jour destructrices pour améliorer l'efficacité. En ce sens, le paradigme impératif est plus proche du métal.
Giorgio
9

L'utilisation de l'état mutable est généralement déconseillée dans la programmation fonctionnelle. Les boucles sont découragées en conséquence, car les boucles ne sont utiles qu'en combinaison avec un état mutable.

La fonction dans son ensemble est pure, ce qui est formidable, mais le paradigme de la programmation fonctionnelle ne s'applique pas seulement au niveau des fonctions entières. Vous voulez également éviter l'état mutable également au niveau local, dans les fonctions. Et le raisonnement est fondamentalement le même: éviter l'état mutable rend le code plus facile à comprendre et empêche certains bogues.

Dans votre cas, vous pourriez écrire numbers.Where(predicate).Sum()ce qui est clairement beaucoup plus simple. Et plus simple signifie moins de bugs.

JacquesB
la source
THX !! Je pense qu'il me manquait une ligne de frappe - mais le paradigme de la programmation fonctionnelle ne s'applique pas seulement au niveau des fonctions entières Mais maintenant je me demande aussi comment visualiser cette frontière. Fondamentalement, du point de vue du consommateur, c'est une fonction pure, mais le développeur qui a réellement écrit cette fonction n'a pas suivi les directives de la fonction pure? confused :(
rahulaga_dev
@RahulAgarwal: Quelle frontière?
JacquesB
Je suis confus dans le sens si le paradigme de programmation peut être considéré comme FP du point de vue du consommateur de fonctions? Bcoz si je regarde l'implémentation de l'implémentation de Wherein numbers.Where(predicate).Sum()- il utilise la foreachboucle.
rahulaga_dev
3
@RahulAgarwal: En tant que consommateur d'une fonction, vous ne vous souciez pas vraiment si une fonction ou un module utilise en interne un état mutable tant qu'il est pur en externe.
JacquesB
7

Bien que vous ayez raison de dire que du point de vue d'un observateur externe, votre Sumfonction est pure, l'implémentation interne n'est clairement pas pure - vous avez un état stocké dans resultlequel vous mutez à plusieurs reprises. L'une des raisons pour éviter un état mutable est qu'il produit une plus grande charge cognitive sur le programmeur, ce qui conduit à son tour à plus de bogues [citation nécessaire] .

Alors que dans un exemple simple comme celui-ci, la quantité d'état mutable stockée est probablement suffisamment petite pour ne pas causer de problèmes graves, le principe général s'applique toujours. Un exemple de jouet comme Sumprobablement n'est pas le meilleur moyen d'illustrer l'avantage de la programmation fonctionnelle par rapport à l'impératif - essayez de faire quelque chose avec beaucoup d'état mutable et les avantages peuvent devenir plus clairs.

Philip Kendall
la source