J'ai du mal à trouver des ressources sur la façon d'écrire des programmes dans un style fonctionnel. Le sujet le plus avancé que j'ai pu trouver discuté en ligne était l'utilisation du typage structurel pour réduire les hiérarchies de classes; la plupart traitent simplement comment utiliser map / fold / réduire / etc pour remplacer les boucles impératives.
Ce que j'aimerais vraiment trouver, c'est une discussion approfondie sur une implémentation OOP d'un programme non trivial, ses limites et comment le refactoriser dans un style fonctionnel. Pas seulement un algorithme ou une structure de données, mais quelque chose avec plusieurs rôles et aspects différents - un jeu vidéo peut-être. Au fait, j'ai lu la programmation fonctionnelle dans le monde réel de Tomas Petricek, mais je m'en veux encore.
Réponses:
Définition de la programmation fonctionnelle
L'introduction de The Joy of Clojure dit ce qui suit:
Programmation dans Scala 2nd Edition p. 10 a la définition suivante:
Si nous acceptons la première définition, alors la seule chose que vous devez faire pour rendre votre code "fonctionnel" est de retourner vos boucles à l'envers. La deuxième définition inclut l'immuabilité.
Fonctions de première classe
Imaginez que vous obtenez actuellement une liste de passagers de votre objet Bus et que vous itérez dessus en diminuant le compte bancaire de chaque passager du montant du tarif du bus. La manière fonctionnelle d'effectuer cette même action serait d'avoir une méthode sur Bus, peut-être appelée forEachPassenger qui prend en fonction un argument. Ensuite, Bus itérerait sur ses passagers, mais cela est préférable et votre code client qui facture le prix du trajet serait mis en fonction et transmis à forEachPassenger. Voila! Vous utilisez une programmation fonctionnelle.
Impératif:
Fonctionnel (en utilisant une fonction anonyme ou "lambda" dans Scala):
Version Scala plus sucrée:
Fonctions non de première classe
Si votre langue ne prend pas en charge les fonctions de première classe, cela peut devenir très moche. Dans Java 7 ou version antérieure, vous devez fournir une interface "Functional Object" comme celle-ci:
Ensuite, la classe Bus fournit un itérateur interne:
Enfin, vous passez un objet fonction anonyme au bus:
Java 8 permet aux variables locales d'être capturées dans le cadre d'une fonction anonyme, mais dans les versions antérieures, ces varibales doivent être déclarées finales. Pour contourner ce problème, vous devrez peut-être créer une classe wrapper MutableReference. Voici une classe spécifique à un entier qui vous permet d'ajouter un compteur de boucles au code ci-dessus:
Même avec cette laideur, il est parfois avantageux d'éliminer la logique compliquée et répétée des boucles réparties dans votre programme en fournissant un itérateur interne.
Cette laideur a été corrigée dans Java 8, mais la gestion des exceptions vérifiées dans une fonction de première classe est toujours vraiment laide et Java porte toujours l'hypothèse de la mutabilité dans toutes ses collections. Ce qui nous amène aux autres objectifs souvent associés à la PF:
Immutabilité
L'article 13 de Josh Bloch est «Préférer l'immuabilité». Malgré le fait que les ordures parlent le contraire, la POO peut être effectuée avec des objets immuables, ce qui le rend beaucoup mieux. Par exemple, String en Java est immuable. StringBuffer, OTOH doit être modifiable pour construire une chaîne immuable. Certaines tâches, comme travailler avec des tampons, nécessitent intrinsèquement une mutabilité.
Pureté
Chaque fonction doit au moins être mémorisable - si vous lui donnez les mêmes paramètres d'entrée (et qu'elle ne devrait avoir aucune entrée en dehors de ses arguments réels), elle devrait produire la même sortie à chaque fois sans provoquer "d'effets secondaires" comme changer l'état global, effectuer I / O, ou lever des exceptions.
Il a été dit que dans la programmation fonctionnelle, "un peu de mal est généralement nécessaire pour faire le travail". La pureté à 100% n'est généralement pas l'objectif. La minimisation des effets secondaires est.
Conclusion
Vraiment, de toutes les idées ci-dessus, l'immuabilité a été la plus grande victoire unique en termes d'applications pratiques pour simplifier mon code - que ce soit OOP ou FP. La transmission de fonctions aux itérateurs est la deuxième plus grande victoire. La documentation Java 8 Lambdas explique le mieux pourquoi. La récursivité est idéale pour traiter les arbres. La paresse vous permet de travailler avec des collections infinies.
Si vous aimez la JVM, je vous recommande de jeter un œil à Scala et Clojure. Les deux sont des interprétations perspicaces de la programmation fonctionnelle. Scala est de type sécurisé avec une syntaxe quelque peu similaire à C, bien qu'il ait vraiment autant de syntaxe en commun avec Haskell qu'avec C. Clojure n'est pas de type sécurisé et c'est un Lisp. J'ai récemment publié une comparaison de Java, Scala et Clojure en ce qui concerne un problème de refactorisation spécifique. La comparaison de Logan Campbell avec Game of Life inclut également Haskell et Clojure.
PS
Jimmy Hoffa a souligné que ma classe de bus est modifiable. Plutôt que de corriger l'original, je pense que cela démontrera exactement le type de refactorisation de cette question. Cela peut être résolu en faisant de chaque méthode sur le bus une usine pour produire un nouveau bus, chaque méthode sur le passager une usine pour produire un nouveau passager. J'ai donc ajouté un type de retour à tout ce qui signifie que je vais copier java.util.function.Function de Java 8 au lieu de l'interface consommateur:
Puis en bus:
Enfin, l'objet fonction anonyme renvoie l'état modifié des choses (un nouveau bus avec de nouveaux passagers). Cela suppose que p.debit () retourne maintenant un nouveau passager immuable avec moins d'argent que l'original:
J'espère que vous pouvez maintenant prendre votre propre décision sur la façon dont vous voulez rendre votre langage impératif fonctionnel, et décider s'il serait préférable de repenser votre projet en utilisant un langage fonctionnel. Dans Scala ou Clojure, les collections et autres API sont conçues pour faciliter la programmation fonctionnelle. Les deux ont une très bonne interopérabilité Java, vous pouvez donc mélanger et faire correspondre les langues. En fait, pour l'interopérabilité Java, Scala compile ses fonctions de première classe en classes anonymes qui sont presque compatibles avec les interfaces fonctionnelles Java 8. Vous pouvez lire les détails dans Scala in Depth sect. 1.3.2 .
la source
J'ai une expérience personnelle "accomplissant" ceci. En fin de compte, je n'ai pas trouvé quelque chose de purement fonctionnel, mais j'ai trouvé quelque chose qui me satisfait. Voici comment je l'ai fait:
x
, faites en sorte que la méthode soit passéex
au lieu d'appelerthis.x
.x.methodThatModifiesTheFooVar()
enfooFn(x.foo)
map
,reduce
,filter
, etc.Je ne pouvais pas me débarrasser de l'état mutable. C'était tout simplement trop non idiomatique dans ma langue (JavaScript). Mais, en faisant passer et / ou renvoyer tous les états, chaque fonction peut être testée. Ceci est différent de la POO où la configuration de l'état prendrait trop de temps ou la séparation des dépendances nécessite souvent de modifier d'abord le code de production.
De plus, je peux me tromper sur la définition, mais je pense que mes fonctions sont référentiellement transparentes: mes fonctions auront le même effet étant donné la même entrée.
modifier
Comme vous pouvez le voir ici , il n'est pas possible de créer un objet vraiment immuable en JavaScript. Si vous êtes diligent et que vous contrôlez qui appelle votre code, vous pouvez le faire en créant toujours un nouvel objet au lieu de muter celui en cours. Cela ne valait pas la peine pour moi.
Mais si vous utilisez Java, vous pouvez utiliser ces techniques pour rendre vos classes immuables.
la source
Je ne pense pas qu'il soit vraiment possible de refactoriser complètement le programme - il faudrait repenser et réimplémenter dans le bon paradigme.
J'ai vu le refactoring de code défini comme une "technique disciplinée pour restructurer un corps de code existant, en modifiant sa structure interne sans changer son comportement externe".
Vous pourriez rendre certaines choses plus fonctionnelles, mais vous avez toujours un programme orienté objet. Vous ne pouvez pas simplement changer de petits morceaux pour l'adapter à un paradigme différent.
la source
Je pense que cette série d'articles est exactement ce que vous voulez:
Rétrogames purement fonctionnels
http://prog21.dadgum.com/23.html Partie 1
http://prog21.dadgum.com/24.html Partie 2
http://prog21.dadgum.com/25.html Partie 3
http://prog21.dadgum.com/26.html Partie 4
http://prog21.dadgum.com/37.html Suivi
Le résumé est:
L'auteur suggère une boucle principale avec des effets secondaires (les effets secondaires doivent se produire quelque part, non?) Et la plupart des fonctions renvoient de petits enregistrements immuables détaillant comment ils ont changé l'état du jeu.
Bien sûr, lors de l'écriture d'un programme du monde réel, vous mélangerez et assortirez plusieurs styles de programmation, en utilisant chacun où cela aide le plus. Cependant, c'est une bonne expérience d'apprentissage pour essayer d'écrire un programme de la manière la plus fonctionnelle / immuable et aussi de l'écrire de la manière la plus spaghetti, en utilisant uniquement des variables globales :-) (faites-le comme une expérience, pas en production, s'il vous plaît)
la source
Vous devrez probablement retourner tout votre code car OOP et FP ont deux approches opposées pour organiser le code.
La POO organise le code autour des types (classes): différentes classes peuvent implémenter la même opération (une méthode avec la même signature). Par conséquent, la POO est plus appropriée lorsque l'ensemble des opérations ne change pas beaucoup alors que de nouveaux types peuvent être ajoutés très souvent. Par exemple, considérons une bibliothèque graphique dans laquelle chaque widget a un ensemble de méthodes fixes (
hide()
,show()
,paint()
,move()
, etc.) , mais de nouveaux widgets pourraient être ajoutés comme la bibliothèque est prolongée. Dans OOP, il est facile d'ajouter un nouveau type (pour une interface donnée): il suffit d'ajouter une nouvelle classe et d'implémenter toutes ses méthodes (changement de code local). D'un autre côté, l'ajout d'une nouvelle opération (méthode) à une interface peut nécessiter de changer toutes les classes qui implémentent cette interface (même si l'héritage peut réduire la quantité de travail).FP organise le code autour des opérations (fonctions): chaque fonction implémente une opération qui peut traiter différents types de différentes manières. Ceci est généralement réalisé en répartissant le type via la correspondance de modèle ou un autre mécanisme. Par conséquent, FP est plus approprié lorsque l'ensemble des types est stable et que de nouvelles opérations sont ajoutées plus souvent. Prenons par exemple un ensemble fixe de formats d'image (GIF, JPEG, etc.) et certains algorithmes que vous souhaitez implémenter. Chaque algorithme peut être implémenté par une fonction qui se comporte différemment selon le type de l'image. L'ajout d'un nouvel algorithme est facile car il vous suffit d'implémenter une nouvelle fonction (changement de code local). L'ajout d'un nouveau format (type) nécessite de modifier toutes les fonctions que vous avez implémentées jusqu'à présent pour le prendre en charge (changement non local).
Conclusion: OOP et FP sont fondamentalement différents dans la façon dont ils organisent le code, et changer une conception OOP en une conception FP impliquerait de changer tout votre code pour refléter cela. Cela peut cependant être un exercice intéressant. Voir également ces notes de cours du livre SICP cité par mikemay, en particulier les diapositives 13.1.5 à 13.1.10.
la source