J'ai récemment appris la programmation fonctionnelle (spécifiquement Haskell, mais j'ai également suivi des tutoriels sur LISP et Erlang). Bien que j'ai trouvé les concepts très instructifs, 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 sauver l'état de manière simple (je ne considère pas les monades de Haskell comme "faciles").
Vaut-il la peine de continuer à apprendre Haskell (ou un autre langage purement fonctionnel) en profondeur? La programmation fonctionnelle ou sans état est-elle réellement plus productive que procédurale? Est-il probable que je continuerai à 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 que procédural/orienté objet/peu importe.
Lire Programmation fonctionnelle en bref .
La programmation sans état présente de nombreux avantages, dont le moindre est considérablement le code multithread et simultané. Pour le dire franchement, l'état mutable est ennemi du code multithread. Si les valeurs sont immuables par défaut, les programmeurs n'ont pas à se soucier d'un thread qui mute la valeur de l'état partagé entre deux threads, ce qui élimine toute une classe de bogues multithreads liés aux conditions de concurrence. Comme 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 grande 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 changent 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 plus agressivement votre code.
Plus il y a de morceaux de votre programme sans état, plus il y a de façons de rassembler les morceaux sans rien casser . La puissance du paradigme des apatrides ne réside pas dans l'apatridie (ou la pureté) en soi, mais la capacité qu'il vous donne d'écrire des fonctions puissantes, réutilisables et de les combiner .
Vous pouvez trouver un bon tutoriel avec de nombreux exemples dans l'article de John Hughes Pourquoi la programmation fonctionnelle est importante (PDF).
Vous serez gobs 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).
Bon nombre des autres réponses se sont concentrées sur le côté performance (parallélisme) de la programmation fonctionnelle, qui je crois 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.
En fait, je trouve (par expérience personnelle) que la programmation en F # correspond à la façon dont je pense mieux, et c'est donc plus facile. Je pense que c'est la plus grande différence. J'ai programmé à la fois en F # et en C #, et il y a beaucoup moins de "lutte contre le langage" en F #, 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 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 à la façon de le mettre en œuvre. En fait, chaque fois que je reviens en 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.
Toujours 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 quoi, c'est d'un puzzle Euler de projet):
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 devez écrire la boucle elle-même, et avoir votre propre instruction if explicite, et ce genre de choses. Depuis l'apprentissage de F #, j'ai réalisé que je trouvais plus facile de coder de manière fonctionnelle, où si vous voulez filtrer, vous écrivez "filtrer", et si vous voulez mapper, vous écrivez "mapper", au lieu d'implémenter chacun des détails.
J'adore également l'opérateur |>, qui je pense sépare F # d'ocaml, et peut-être d'autres langages fonctionnels. C'est l'opérateur de pipe, il vous permet de "pipe" la sortie d'une expression dans l'entrée d'une autre expression. Cela fait que le code suit ce que je pense de plus. Comme dans l'extrait de code ci-dessus, c'est-à-dire "prenez la séquence des 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 seule 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 plus facile d'utiliser F # car vous pouvez penser à un niveau supérieur. Je dirais que parce que les petits détails sont supprimés de la programmation fonctionnelle (en F # au moins), je suis plus productif.
Edit: J'ai vu dans l'un des commentaires que vous avez demandé un exemple de "é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
Considérez tous les bogues difficiles que vous avez mis au point depuis longtemps.
Maintenant, combien de ces bogues étaient dus à des "interactions involontaires" entre deux composants distincts d'un programme? (Presque tous les bugs de threading ont cette forme: races 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 qui lisent/écrivent le registre/l'environnement, etc.) - [[# #]] i [~ # ~] supposerait qu'au moins 1 sur 3 "bugs durs" entrent dans cette catégorie.
Maintenant, si vous passez à une programmation sans état/immuable/pure, tous ces bogues disparaissent. Vous êtes plutôt confronté à de nouveaux défis (par exemple, lorsque vous faites voulez que différents modules interagissent avec l'environnement), mais dans un langage comme Haskell, ces interactions sont explicitement réifiées dans le système de types, ce qui signifie que vous peut simplement regarder le type d'une fonction et expliquer le type d'interactions qu'elle peut avoir avec le reste du programme.
C'est la grande victoire de l'OMI "immuabilité". Dans un monde idéal, nous concevrions tous de formidables API et même lorsque les choses étaient mutables, 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 coulisses entre les composants.
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 à mettre en parallèle.
Mais l'efficacité n'est pas la seule préoccupation. Une fonction pure est plus facile à tester et à déboguer car tout ce qui l'affecte est explicitement indiqué. Et quand on programme dans un langage fonctionnel, on prend l'habitude de rendre le moins de fonctions "sales" (avec E/S, etc.) possible. Séparer les éléments avec état de cette façon est un bon moyen de concevoir des programmes, même dans des langages moins 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 de langages fonctionnels.
Sans état, il est très facile de paralléliser automatiquement votre code (comme les CPU sont faits avec de plus en plus de cœurs, c'est très important).
Les applications Web sans état sont essentielles lorsque vous commencez à avoir un trafic plus important.
Il peut y avoir de nombreuses 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 cependant pas idéal, par exemple, cela signifie que chaque fois que vous redémarrez votre application Web, tous les utilisateurs connectés perdent leur session.
Une meilleure approche consiste à stocker la session derrière les serveurs Web dans une sorte de magasin de données, ces jours-ci, il existe de nombreux excellents 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 magasin 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.
J'ai écrit un article sur ce sujet il y a quelque temps: Sur l'importance de la pureté .