N'oubliez pas : la programmation fonctionnelle n'est pas une programmation avec des fonctions ! ! !
23.4 Programmation fonctionnelle
23.4.1 Qu'est-ce que la programmation fonctionnelle
? Si vous posez la question sans détour, vous constaterez que c’est un concept qui n’est pas facile à expliquer. De nombreux vétérans possédant de nombreuses années d'expérience dans le domaine de la programmation ne peuvent pas expliquer clairement ce qu'est la programmation fonctionnelle. La programmation fonctionnelle est en effet un domaine peu familier pour les programmeurs familiers avec la programmation procédurale. Les concepts de fermeture, de continuation et de currying nous semblent si peu familiers. Les familiers if, else et while n'ont rien en commun. Bien que la programmation fonctionnelle possède de magnifiques prototypes mathématiques que la programmation procédurale ne peut égaler, elle est si mystérieuse que seuls les titulaires d'un doctorat peuvent la maîtriser.
Astuce : Cette section est un peu difficile, mais ce n'est pas une compétence nécessaire pour maîtriser JavaScript. Si vous ne souhaitez pas utiliser JavaScript pour effectuer les tâches effectuées en Lisp, ou si vous ne souhaitez pas acquérir les compétences ésotériques de. programmation fonctionnelle, vous pouvez l'ignorer. Parcourez-les et entrez dans le prochain chapitre de votre voyage.
Revenons donc à la question : qu’est-ce que la programmation fonctionnelle ? La réponse est longue…
La première loi de la programmation fonctionnelle : Les fonctions sont du premier type.
Comment comprendre cette phrase elle-même ? Qu’est-ce qu’un vrai Type One ? Examinons les concepts mathématiques suivants :
équation binaire F(x, y) = 0, x, y sont des variables, écrivons-la comme y = f(x), x est un paramètre, y est la valeur de retour, f vient de x à y La relation de mappage est appelée une fonction. Si tel est le cas, G(x, y, z) = 0 ou z = g(x, y), g est la relation de mappage de x, y à z, et est également une fonction. Si les paramètres x et y de g satisfont la relation précédente y = f(x), alors nous obtenons z = g(x, y) = g(x, f(x)). L'une est f(. x) est une fonction sur x et un paramètre de la fonction g. Deuxièmement, g est une fonction d'ordre supérieur à f.
De cette façon, nous utilisons z = g(x, f(x)) pour représenter la solution associée des équations F(x, y) = 0 et G(x, y, z) = 0, qui est une fonction itérative . Nous pouvons également exprimer g sous une autre forme, rappelez-vous z = g(x, y, f), afin de généraliser la fonction g en une fonction d'ordre supérieur. Par rapport à la précédente, l'avantage de cette dernière représentation est qu'il s'agit d'un modèle plus général, comme la solution associée de T(x,y) = 0 et G(x,y,z) = 0. On peut être exprimé sous la même forme (soit simplement f=t). Dans ce système linguistique qui prend en charge l'itération de conversion de la solution d'un problème en une fonction d'ordre supérieur, la fonction est appelée « premier type ».
Les fonctions en JavaScript sont clairement du « premier type ». Voici un exemple typique :
Array.prototype.each = fonction (fermeture)
{
return this.length ? [closure(this[0])].concat(this.slice(1).each(closure)) : [];
}
C'est vraiment un code magique, qui fait jouer pleinement le charme du style fonctionnel. Il n'y a que des fonctions et des symboles dans tout le code. C'est simple dans sa forme et infiniment puissant.
[1,2,3,4].each(function(x){return x * 2}) obtient [2,4,6,8], tandis que [1,2,3,4].each(function(x ){return x-1}) obtient [0,1,2,3].
L'essence de la fonctionnalité et de l'orientation objet est que « Tao suit la nature ». Si l'orientation objet est une simulation du monde réel, alors l'expression fonctionnelle est une simulation du monde mathématique. Dans un sens, son niveau d'abstraction est supérieur à celui de l'orientation objet, car les systèmes mathématiques ont intrinsèquement des caractéristiques de nature incomparable. d'abstraction.
La deuxième loi de la programmation fonctionnelle : les fermetures sont les meilleures amies de la programmation fonctionnelle.
Les fermetures, comme nous l'avons expliqué dans les chapitres précédents, sont très importantes pour la programmation fonctionnelle. Sa plus grande fonctionnalité est que vous pouvez accéder directement à l'environnement externe depuis la couche interne sans passer de variables (symboles). Cela apporte une grande commodité aux programmes fonctionnels sous imbrication multiple. Voici un exemple :
(fonction externalFun(x)).
{
fonction de retour innerFun(y)
{
renvoyer x * y ;
}
})(2)(3);
La troisième loi de la programmation fonctionnelle : les fonctions peuvent être du Currying.
Qu'est-ce que le curry ? C'est un concept intéressant. Commençons par les mathématiques : disons, considérons une équation spatiale tridimensionnelle F(x, y, z) = 0, si nous limitons z = 0, alors nous obtenons F(x, y, 0) = 0, noté F '(x, y). Ici F' est évidemment une nouvelle équation, qui représente la projection bidimensionnelle de la courbe spatiale tridimensionnelle F(x, y, z) sur le plan z = 0. Notons y = f(x, z), soit z = 0, nous obtenons y = f(x, 0), notons-le comme y = f'(x), nous disons que la fonction f' est une solution de Currying de f .
Un exemple de JavaScript Currying est donné ci-dessous :
fonction ajouter (x, y)
{
if(x!=null && y!=null) return x + y;
sinon if(x!=null && y==null) renvoie la fonction(y)
{
renvoyer x + y ;
}
sinon if(x==null && y!=null) renvoie la fonction(x)
{
renvoyer x + y ;
}
}
var a = ajouter (3, 4);
var b = ajouter(2);
var c = b(10);
Dans l'exemple ci-dessus, b=add(2) donne une fonction Currying de add(), qui est fonction du paramètre y lorsque x = 2. Notez qu'elle est également utilisée ci-dessus. de fermetures.
Fait intéressant, nous pouvons généraliser le Currying pour n'importe quelle fonction, par exemple :
function Foo(x, y, z, w)
{
var args = arguments;
if(Foo.length < args.length)
fonction de retour()
{
retour
args.callee.apply(Array.apply([], args).concat(Array.apply([], arguments)));
}
autre
retourner x + y – z * w ;
}
La quatrième loi de la programmation fonctionnelle : évaluation et continuation différées.
//À FAIRE : Pensez-y à nouveau ici
23.4.2 Avantages des
tests unitaires
de programmation fonctionnelleChaque symbole de programmation fonctionnelle stricte est une référence à une quantité directe ou à un résultat d'expression, et aucune fonction n'a d'effets secondaires. Parce que la valeur n'est jamais modifiée quelque part et qu'aucune fonction ne modifie une quantité en dehors de sa portée qui est utilisée par d'autres fonctions (telles que des membres de classe ou des variables globales). Cela signifie que le résultat de l'évaluation d'une fonction est uniquement sa valeur de retour et que les seuls éléments qui affectent sa valeur de retour sont les paramètres de la fonction.
C’est le rêve humide d’un testeur unitaire. Pour chaque fonction du programme testé, il vous suffit de vous soucier de ses paramètres, sans avoir à prendre en compte l'ordre des appels de fonction ni à définir soigneusement l'état externe. Tout ce que vous avez à faire est de transmettre des paramètres qui représentent des cas extrêmes. Si chaque fonction du programme réussit les tests unitaires, vous avez une confiance considérable dans la qualité du logiciel. Mais la programmation impérative ne peut pas être aussi optimiste. En Java ou en C++, il ne suffit pas de vérifier la valeur de retour d'une fonction : il faut également vérifier l'état externe que la fonction a pu modifier.
Débogage
Si un programme fonctionnel ne se comporte pas comme prévu, le débogage est un jeu d'enfant. Étant donné que les bogues dans les programmes fonctionnels ne dépendent pas de chemins de code qui ne leur sont pas liés avant leur exécution, les problèmes que vous rencontrez peuvent toujours être reproduits. Dans les programmes impératifs, des bugs apparaissent et disparaissent, car le fonctionnement de la fonction dépend des effets secondaires d'autres fonctions, et vous pouvez chercher longtemps dans des directions sans rapport avec l'apparition du bug, mais sans aucun résultat. Ce n'est pas le cas des programmes fonctionnels : si le résultat d'une fonction est erroné, peu importe ce que vous exécutez auparavant, la fonction renverra toujours le même résultat erroné.
Une fois que vous aurez recréé le problème, trouver sa cause profonde se fera sans effort et pourrait même vous rendre heureux. Interrompez l'exécution de ce programme et examinez la pile Comme pour la programmation impérative, les paramètres de chaque appel de fonction sur la pile vous sont présentés. Mais dans les programmes impératifs, ces paramètres ne suffisent pas. Les fonctions dépendent également des variables membres, des variables globales et de l'état de la classe (qui à son tour dépend de plusieurs d'entre elles). En programmation fonctionnelle, une fonction ne dépend que de ses paramètres, et cette information est sous vos yeux ! De plus, dans un programme impératif, le simple fait de vérifier la valeur de retour d'une fonction ne peut pas vous assurer que la fonction fonctionne correctement. Vous devez vérifier l'état de dizaines d'objets en dehors de la portée de cette fonction pour confirmer. Avec un programme fonctionnel, il suffit de regarder sa valeur de retour !
Vérifiez les paramètres et les valeurs de retour de la fonction le long de la pile. Dès que vous trouvez un résultat déraisonnable, entrez cette fonction et suivez-la étape par étape jusqu'à ce que vous trouviez le point où le bug est généré.
Les programmes fonctionnels parallèles peuvent être exécutés en parallèle sans aucune modification. Ne vous inquiétez pas des blocages et des sections critiques, car vous n'utilisez jamais de verrous ! Aucune donnée d'un programme fonctionnel n'est modifiée deux fois par le même thread, encore moins par deux threads différents. Cela signifie que des threads peuvent être simplement ajoutés sans arrière-pensée, sans provoquer les problèmes traditionnels qui affligent les applications parallèles.
Si tel est le cas, pourquoi tout le monde n'utilise-t-il pas la programmation fonctionnelle dans des applications nécessitant des opérations hautement parallèles ? Eh bien, c'est ce qu'ils font. Ericsson a conçu un langage fonctionnel appelé Erlang et l'a utilisé dans des commutateurs de télécommunications qui nécessitent une tolérance aux pannes et une évolutivité extrêmement élevées. De nombreuses personnes ont également découvert les avantages d’Erlang et ont commencé à l’utiliser. Nous parlons de systèmes de contrôle de télécommunications, qui nécessitent bien plus de fiabilité et d'évolutivité qu'un système typique conçu pour Wall Street. En fait, le système Erlang n'est pas fiable et extensible, JavaScript l'est. Les systèmes Erlang sont tout simplement solides.
L'histoire du parallélisme ne s'arrête pas là. Même si votre programme est monothread, un compilateur de programme fonctionnel peut toujours l'optimiser pour qu'il s'exécute sur plusieurs processeurs. Veuillez consulter le code suivant :
String s1 = quelque peuLongOperation1();
Chaîne s2 = quelque peuLongOperation2();
String s3 = concatenate(s1, s2);
Dans un langage de programmation fonctionnel, le compilateur analyse le code pour identifier les fonctions potentiellement chronophages qui créent les chaînes s1 et s2, puis les exécute en parallèle. Cela n'est pas possible dans les langages impératifs, où chaque fonction peut modifier son état en dehors de la portée de la fonction et où les fonctions ultérieures peuvent dépendre de ces modifications. Dans les langages fonctionnels, analyser automatiquement les fonctions et identifier les candidats adaptés à une exécution parallèle est aussi simple que l'inline automatique de fonctions ! En ce sens, la programmation de style fonctionnel est « à l’épreuve du temps » (même si je n’aime pas utiliser les termes de l’industrie, je ferai une exception cette fois). Les fabricants de matériel ne pouvaient plus accélérer le fonctionnement des processeurs, ils ont donc augmenté la vitesse des cœurs de processeur et ont quadruplé la vitesse grâce au parallélisme. Bien sûr, ils ont également oublié de mentionner que l’argent supplémentaire que nous avons dépensé était uniquement utilisé pour acheter des logiciels permettant de résoudre des problèmes parallèles. Une faible proportion de logiciels impératifs et de logiciels 100% fonctionnels peuvent tourner directement en parallèle sur ces machines.
Le déploiement à chaud du code
nécessitait auparavant l'installation de mises à jour sous Windows et le redémarrage de l'ordinateur était inévitable, et ce à plusieurs reprises, même si une nouvelle version du lecteur multimédia était installée. Windows XP a grandement amélioré cette situation, mais ce n'est toujours pas idéal (j'ai exécuté Windows Update au travail aujourd'hui, et maintenant une icône ennuyeuse apparaît toujours dans la barre d'état à moins que je ne redémarre la machine). Les systèmes Unix ont toujours fonctionné dans un meilleur mode lors de l'installation de mises à jour, seuls les composants liés au système doivent être arrêtés, plutôt que l'ensemble du système d'exploitation. Même ainsi, cela reste insatisfaisant pour une application serveur à grande échelle. Les systèmes de télécommunications doivent être opérationnels 100 % du temps, car si la numérotation d'urgence échoue pendant la mise à jour du système, des pertes de vie pourraient en résulter. Il n'y a aucune raison pour que les entreprises de Wall Street soient obligées de fermer leurs services pendant le week-end pour installer des mises à jour.
La situation idéale est de mettre à jour le code concerné sans arrêter aucun composant du système. C’est impossible dans un monde impératif. Considérez que lorsque le runtime télécharge une classe Java et remplace une nouvelle définition, toutes les instances de cette classe seront indisponibles car leur état enregistré est perdu. Nous pourrions commencer à écrire du code de contrôle de version fastidieux pour résoudre ce problème, puis sérialiser toutes les instances de cette classe, détruire ces instances, puis recréer ces instances avec la nouvelle définition de cette classe, puis charger les données précédemment sérialisées et espérer que le chargement le code portera correctement ces données vers la nouvelle instance. De plus, le code de portage doit être réécrit manuellement à chaque mise à jour, et il faut faire très attention à ne pas rompre les interrelations entre les objets. La théorie est simple, mais la pratique n’est pas facile.
Pour les programmes fonctionnels, tous les états, c'est-à-dire les paramètres passés à la fonction, sont enregistrés sur la pile, ce qui rend le déploiement à chaud un jeu d'enfant ! En fait, tout ce que nous avons à faire est de faire une différence entre le code fonctionnel et la nouvelle version, puis de déployer le nouveau code. Le reste sera fait automatiquement par un outil linguistique ! Si vous pensez qu’il s’agit d’une histoire de science-fiction, détrompez-vous. Depuis des années, les ingénieurs d'Erlang mettent à jour leurs systèmes en cours d'exécution sans l'interrompre.
Raisonnement et optimisation assistés par machine
Une propriété intéressante des langages fonctionnels est qu'ils peuvent être raisonnés mathématiquement. Parce qu'un langage fonctionnel n'est qu'une implémentation d'un système formel, toutes les opérations effectuées sur papier peuvent être appliquées à des programmes écrits dans ce langage. Les compilateurs peuvent utiliser la théorie mathématique pour transformer un morceau de code en code équivalent mais plus efficace [7]. Les bases de données relationnelles subissent ce type d’optimisation depuis des années. Il n'y a aucune raison pour que cette technique ne puisse pas être appliquée aux logiciels classiques.
De plus, vous pouvez utiliser ces techniques pour prouver que certaines parties de votre programme sont correctes, et peut-être même créer des outils pour analyser votre code et générer automatiquement des cas extrêmes pour les tests unitaires ! Cette fonctionnalité n'a aucune valeur pour un système robuste, mais si vous concevez un stimulateur cardiaque ou un système de contrôle du trafic aérien, cet outil est indispensable. Si les applications que vous écrivez ne constituent pas des tâches essentielles dans l’industrie, ce type d’outil peut également vous donner un atout sur vos concurrents.
23.4.3 Inconvénients de la programmation fonctionnelle
Effets secondaires des fermetures
Dans la programmation fonctionnelle non stricte, les fermetures peuvent outrepasser l'environnement externe (nous l'avons déjà vu dans le chapitre précédent), ce qui entraîne des effets secondaires, et lorsque ces effets secondaires se produisent fréquemment Et quand l'environnement dans lequel le programme s'exécute est fréquemment modifié, les erreurs deviennent difficiles à suivre.
//TODO :
forme récursive
Bien que la récursion soit souvent la forme d’expression la plus concise, elle n’est pas aussi intuitive que les boucles non récursives.
//TODO :
La faiblesse de la valeur retardée
//TODO :