Avantages de la programmation sans état?

132

J'ai récemment appris la programmation fonctionnelle (en particulier Haskell, mais j'ai également suivi des tutoriels sur Lisp et Erlang). Bien que je trouve les concepts très éclairants, je ne vois toujours pas le côté pratique du concept «sans effets secondaires». Quels en sont les avantages pratiques? J'essaie de penser dans un état d'esprit fonctionnel, mais il y a des situations qui semblent trop complexes sans la possibilité de sauvegarder l'état de manière simple (je ne considère pas les monades de Haskell comme «faciles»).

Vaut-il la peine de continuer à apprendre en profondeur Haskell (ou un autre langage purement fonctionnel)? La programmation fonctionnelle ou sans état est-elle réellement plus productive que procédurale? Est-il probable que je continue à utiliser Haskell ou un autre langage fonctionnel plus tard, ou devrais-je l'apprendre uniquement pour la compréhension?

Je me soucie moins des performances que de la productivité. Je demande donc principalement si je serai plus productif dans un langage fonctionnel qu'un procédural / orienté objet / peu importe.

Sasha Chedygov
la source

Réponses:

168

Lisez la programmation fonctionnelle en un mot .

Il y a beaucoup d'avantages à la programmation sans état, et non des moindres est considérablement le code multithread et en même temps. Pour le dire franchement, l'état mutable est l'ennemi du code multithread. Si les valeurs sont immuables par défaut, les programmeurs n'ont pas besoin de s'inquiéter du fait qu'un thread mute la valeur de l'état partagé entre deux threads, ce qui élimine toute une classe de bogues multithreading liés aux conditions de concurrence. Puisqu'il n'y a pas de conditions de concurrence, il n'y a aucune raison d'utiliser des verrous non plus, donc l'immuabilité élimine également une autre classe entière de bogues liés aux blocages.

C'est la principale raison pour laquelle la programmation fonctionnelle est importante, et probablement la meilleure pour sauter dans le train de la programmation fonctionnelle. Il existe également de nombreux autres avantages, notamment un débogage simplifié (c'est-à-dire que les fonctions sont pures et ne mutent pas d'état dans d'autres parties d'une application), un code plus concis et expressif, moins de code standard par rapport aux langages qui dépendent fortement des modèles de conception, et le compilateur peut optimiser votre code de manière plus agressive.

Juliette
la source
5
J'appuie cela! Je crois que la programmation fonctionnelle sera utilisée beaucoup plus largement à l'avenir en raison de son aptitude à la programmation parallèle.
Ray Hidayat
@Ray: J'ajouterais également la programmation distribuée!
Anton Tykhyy
La plupart de cela est vrai, sauf pour le débogage. C'est généralement plus difficile dans haskell, car vous n'avez pas de vraie pile d'appels, juste une pile de correspondance de motif. Et il est beaucoup plus difficile de prédire ce que sera votre code.
hasufell
3
De plus, la programmation fonctionnelle ne concerne pas vraiment «sans état». La récursivité est déjà un état implicite (local) et c'est la principale chose que nous faisons dans haskell. Cela devient clair une fois que vous implémentez quelques algorithmes non triviaux dans haskell idiomatique (par exemple, des trucs de géométrie de calcul) et que vous vous amusez à les déboguer.
hasufell
2
Je n'aime pas assimiler apatride à PF. De nombreux programmes de PF sont remplis d'état, il existe simplement dans une fermeture plutôt qu'un objet.
mikemaccana
46

Plus il y a de morceaux de votre programme sans état, plus il y a de façons de les assembler sans rien casser . La puissance du paradigme sans état ne réside pas dans l'apatridie (ou la pureté) en soi , mais dans la capacité qu'il vous donne à écrire des fonctions puissantes et réutilisables et à les combiner.

Vous pouvez trouver un bon tutoriel avec de nombreux exemples dans l'article de John Hughes Why Functional Programming Matters (PDF).

Vous serez beaucoup plus productif, surtout si vous choisissez un langage fonctionnel qui a également des types de données algébriques et des correspondances de motifs (Caml, SML, Haskell).

Norman Ramsey
la source
Les mixins ne fourniraient-ils pas également du code réutilisable de la même manière avec la POO? Ne pas préconiser la POO en essayant simplement de comprendre les choses moi-même.
mikemaccana
20

La plupart des autres réponses se sont concentrées sur le côté performance (parallélisme) de la programmation fonctionnelle, ce que je pense est très important. Cependant, vous avez spécifiquement posé des questions sur la productivité, comme dans, pouvez-vous programmer la même chose plus rapidement dans un paradigme fonctionnel que dans un paradigme impératif.

Je trouve en fait (par expérience personnelle) que la programmation en F # correspond mieux à ma façon de penser, et donc c'est plus facile. Je pense que c'est la plus grande différence. J'ai programmé à la fois en F # et C #, et il y a beaucoup moins de "combat contre le langage" en F #, ce que j'adore. Vous n'avez pas à penser aux détails en F #. Voici quelques exemples de ce que j'ai trouvé que j'apprécie vraiment.

Par exemple, même si F # est typé statiquement (tous les types sont résolus au moment de la compilation), l'inférence de type détermine quels types vous avez, vous n'avez donc pas à le dire. Et s'il ne peut pas le comprendre, il rend automatiquement votre fonction / classe / quel que soit le générique. Vous n'avez donc jamais à écrire de générique, tout est automatique. Je trouve que cela signifie que je passe plus de temps à réfléchir au problème et moins à le mettre en œuvre. En fait, chaque fois que je reviens à C #, je trouve que cette inférence de type me manque vraiment, vous ne réalisez jamais à quel point c'est distrayant tant que vous n'avez plus besoin de le faire.

Aussi en F #, au lieu d'écrire des boucles, vous appelez des fonctions. C'est un changement subtil, mais significatif, car vous n'avez plus à penser à la construction de la boucle. Par exemple, voici un morceau de code qui passerait et correspondrait à quelque chose (je ne me souviens pas de quoi, c'est d'un puzzle de projet Euler):

let matchingFactors =
    factors
    |> Seq.filter (fun x -> largestPalindrome % x = 0)
    |> Seq.map (fun x -> (x, largestPalindrome / x))

Je me rends compte que faire un filtre puis une carte (c'est une conversion de chaque élément) en C # serait assez simple, mais il faut penser à un niveau inférieur. En particulier, vous devrez écrire la boucle elle-même et avoir votre propre instruction if explicite, et ce genre de choses. Depuis l'apprentissage de F #, je me suis rendu compte que je trouvais plus facile de coder de manière fonctionnelle, où si vous voulez filtrer, vous écrivez "filtre", et si vous voulez mapper, vous écrivez "carte", au lieu d'implémenter chacun des détails.

J'adore aussi l'opérateur |>, qui, je pense, sépare F # d'ocaml, et peut-être d'autres langages fonctionnels. C'est l'opérateur pipe, il vous permet de "canaliser" la sortie d'une expression dans l'entrée d'une autre expression. Cela fait que le code suit ce que je pense davantage. Comme dans l'extrait de code ci-dessus, c'est-à-dire "prenez la séquence de facteurs, filtrez-la, puis mappez-la". C'est un niveau de réflexion très élevé, que vous n'obtenez pas dans un langage de programmation impératif parce que vous êtes tellement occupé à écrire la boucle et les instructions if. C'est la chose qui me manque le plus chaque fois que je vais dans une autre langue.

Donc, en général, même si je peux programmer à la fois en C # et en F #, je trouve qu'il est plus facile d'utiliser F # parce que vous pouvez penser à un niveau supérieur. Je dirais que parce que les plus petits détails sont supprimés de la programmation fonctionnelle (au moins en F #), je suis plus productif.

Edit : J'ai vu dans l'un des commentaires que vous avez demandé un exemple d '"état" dans un langage de programmation fonctionnel. F # peut être écrit impérativement, voici donc un exemple direct de la façon dont vous pouvez avoir un état mutable en F #:

let mutable x = 5
for i in 1..10 do
    x <- x + i
Ray Hidayat
la source
1
Je suis d'accord avec votre message en général, mais |> n'a rien à voir avec la programmation fonctionnelle en soi. En fait, a |> b (p1, p2)c'est juste du sucre syntaxique pour b (a, p1, p2). Ajoutez à cela la bonne associativité et vous l'avez.
Anton Tykhyy
2
Certes, je dois reconnaître que probablement une grande partie de mon expérience positive avec F # a plus à voir avec F # qu'avec la programmation fonctionnelle. Mais encore, il y a une forte corrélation entre les deux, et même si des choses comme l'inférence de type et |> ne sont pas de la programmation fonctionnelle en soi, je dirais certainement qu'elles «vont avec le territoire». Au moins en général.
Ray Hidayat
|> est juste une autre fonction d'infixe d'ordre supérieur, dans ce cas un opérateur d'application de fonction. La définition de vos propres opérateurs infixes d'ordre supérieur fait définitivement partie de la programmation fonctionnelle (sauf si vous êtes un Schemer). Haskell a son $ qui est le même sauf que les informations du pipeline circulent de droite à gauche.
Norman Ramsey
15

Considérez tous les bogues difficiles que vous avez consacrés au débogage depuis longtemps.

Maintenant, combien de ces bogues étaient dus à des «interactions involontaires» entre deux composants séparés d'un programme? (Presque tous les bogues de thread ont cette forme: courses impliquant l'écriture de données partagées, blocages, ... De plus, il est courant de trouver des bibliothèques qui ont un effet inattendu sur l'état global, ou lire / écrire le registre / l'environnement, etc.) I postulerait qu'au moins 1 «bogue dur» sur 3 entre dans cette catégorie.

Maintenant, si vous passez à la programmation sans état / immuable / pure, tous ces bogues disparaissent. On vous présente de nouveaux défis au lieu (par exemple , lorsque vous ne voulez différents modules pour interagir avec l'environnement), mais dans un langage comme Haskell, ces interactions se réifiées explicitement dans le système de type, ce qui signifie que vous pouvez juste regarder le type de une fonction et une raison sur le type d'interactions qu'il peut avoir avec le reste du programme.

C'est la grande victoire de l'OMI «immuabilité». Dans un monde idéal, nous concevrions tous des API formidables et même lorsque les choses étaient modifiables, les effets seraient locaux et bien documentés et les interactions `` inattendues '' seraient réduites au minimum. Dans le monde réel, il existe de nombreuses API qui interagissent avec l'état global de multiples façons, et elles sont à l'origine des bogues les plus pernicieux. Aspirer à l'apatridie, c'est aspirer à se débarrasser des interactions involontaires / implicites / en coulisse entre les composants.

Brian
la source
6
Quelqu'un a dit un jour que l'écrasement d'une valeur modifiable signifie que vous collectez / libérez explicitement la valeur précédente. Dans certains cas, d'autres parties du programme n'ont pas été effectuées en utilisant cette valeur. Lorsque les valeurs ne peuvent pas être mutées, cette classe de bogues disparaît également.
shapr
8

Un avantage des fonctions sans état est qu'elles permettent le précalcul ou la mise en cache des valeurs de retour de la fonction. Même certains compilateurs C vous permettent de marquer explicitement les fonctions comme sans état pour améliorer leur optimisabilité. Comme beaucoup d'autres l'ont noté, les fonctions sans état sont beaucoup plus faciles à paralléliser.

Mais l'efficacité n'est pas la seule préoccupation. Une fonction pure est plus facile à tester et à déboguer puisque tout ce qui l'affecte est explicitement indiqué. Et lors de la programmation dans un langage fonctionnel, on prend l'habitude de rendre le moins de fonctions possible "sales" (avec E / S, etc.). Séparer les éléments avec état de cette manière est un bon moyen de concevoir des programmes, même dans des langages pas si fonctionnels.

Les langages fonctionnels peuvent prendre un certain temps à «obtenir», et il est difficile d'expliquer à quelqu'un qui n'a pas traversé ce processus. Mais la plupart des gens qui persistent assez longtemps réalisent finalement que le tapage en vaut la peine, même s'ils ne finissent pas par utiliser beaucoup les langages fonctionnels.

Artelius
la source
Cette première partie est un point vraiment intéressant, je n'y avais jamais pensé auparavant. Merci!
Sasha Chedygov
Supposons que vous ayez sin(PI/3)dans votre code, où PI est une constante, le compilateur pourrait évaluer cette fonction au moment de la compilation et incorporer le résultat dans le code généré.
Artelius
6

Sans état, il est très facile de paralléliser automatiquement votre code (comme les processeurs sont fabriqués avec de plus en plus de cœurs, c'est très important).

Zifre
la source
Oui, j'ai certainement examiné cela. Le modèle de concurrence d'Erlang en particulier est très intriguant. Cependant, à ce stade, je ne me soucie pas vraiment de la concurrence autant que de la productivité. Y a-t-il un bonus de productivité de la programmation sans état?
Sasha Chedygov
2
@musicfreak, non il n'y a pas de bonus de productivité. Mais à noter, les langages FP modernes vous permettent toujours d'utiliser state si vous en avez vraiment besoin.
Inconnu
Vraiment? Pouvez-vous donner un exemple d'état dans un langage fonctionnel, juste pour que je puisse voir comment c'est fait?
Sasha Chedygov
Découvrez la Monade d'État à Haskell - book.realworldhaskell.org/read/monads.html#x_NZ
rampion
4
@ Inconnu: je ne suis pas d'accord. La programmation sans état réduit la survenue de bogues dus à des interactions imprévues / non intentionnelles de différents composants. Cela encourage également une meilleure conception (plus de réutilisabilité, séparation du mécanisme et de la politique, et ce genre de choses). Ce n'est pas toujours approprié pour la tâche à accomplir, mais dans certains cas, ça brille vraiment.
Artelius
6

Les applications Web sans état sont essentielles lorsque vous commencez à avoir un trafic plus élevé.

Il peut y avoir beaucoup de données utilisateur que vous ne souhaitez pas stocker côté client pour des raisons de sécurité par exemple. Dans ce cas, vous devez le stocker côté serveur. Vous pouvez utiliser la session par défaut des applications Web, mais si vous disposez de plusieurs instances de l'application, vous devrez vous assurer que chaque utilisateur est toujours dirigé vers la même instance.

Les équilibreurs de charge ont souvent la possibilité d'avoir des `` sessions persistantes '' où l'équilibreur de charge sait à quel serveur envoyer la demande des utilisateurs. Ce n'est pas l'idéal, par exemple, cela signifie qu'à chaque fois que vous redémarrez votre application Web, tous les utilisateurs connectés perdront leur session.

Une meilleure approche consiste à stocker la session derrière les serveurs Web dans une sorte de magasin de données, de nos jours, il existe de nombreux produits nosql disponibles pour cela (redis, mongo, elasticsearch, memcached). De cette façon, les serveurs Web sont sans état mais vous avez toujours un état côté serveur et la disponibilité de cet état peut être gérée en choisissant la bonne configuration de banque de données. Ces magasins de données ont généralement une grande redondance, il devrait donc presque toujours être possible d'apporter des modifications à votre application Web et même au magasin de données sans affecter les utilisateurs.

shmish111
la source