Une introduction aux principes de base de la programmation fonctionnelle

Après avoir longtemps appris et travaillé avec la programmation orientée objet, j'ai pris du recul pour réfléchir à la complexité du système.

" Complexity is anything that makes software hard to understand or to modify." - John Outerhout

En faisant quelques recherches, j'ai trouvé des concepts de programmation fonctionnelle comme l'immuabilité et la fonction pure. Ces concepts sont de grands avantages pour créer des fonctions sans effets secondaires, il est donc plus facile de maintenir les systèmes - avec d'autres avantages.

Dans cet article, je vais vous en dire plus sur la programmation fonctionnelle et quelques concepts importants, avec de nombreux exemples de code.

Cet article utilise Clojure comme exemple de langage de programmation pour expliquer la programmation fonctionnelle. Si vous n'êtes pas à l'aise avec un type de langage LISP, j'ai également publié le même article en JavaScript. Jetez un œil: Principes de programmation fonctionnelle en Javascript

Qu'est-ce que la programmation fonctionnelle?

La programmation fonctionnelle est un paradigme de programmation - un style de construction de la structure et des éléments de programmes informatiques - qui traite le calcul comme l'évaluation de fonctions mathématiques et évite les changements d'état et les données mutables - Wikipedia

Fonctions pures

Le premier concept fondamental que nous apprenons lorsque nous voulons comprendre la programmation fonctionnelle est celui des fonctions pures . Mais qu'est-ce que cela signifie réellement? Qu'est-ce qui rend une fonction pure?

Alors, comment savoir si une fonction l'est pureou non? Voici une définition très stricte de la pureté:

  • Il renvoie le même résultat si on lui donne les mêmes arguments (il est également appelé deterministic)
  • Il ne provoque aucun effet secondaire observable

Il renvoie le même résultat si on lui donne les mêmes arguments

Imaginez que nous voulons implémenter une fonction qui calcule l'aire d'un cercle. Une fonction impure recevrait radiuscomme paramètre, puis calculerait radius * radius * PI. Dans Clojure, l'opérateur vient en premier, alors radius * radius * PIdevient (* radius radius PI):

Pourquoi est-ce une fonction impure? Tout simplement parce qu'il utilise un objet global qui n'a pas été passé en paramètre à la fonction.

Imaginez maintenant que certains mathématiciens soutiennent que la PIvaleur est en fait 42et modifient la valeur de l'objet global.

Notre fonction impure aboutira maintenant à 10 * 10 * 42= 4200. Pour le même paramètre ( radius = 10), nous avons un résultat différent. Fixons-le!

TA-DA?! Maintenant, nous passerons toujours la I valeur P en tant que paramètre à la fonction. Alors maintenant, nous accédons simplement aux paramètres passés à la fonction. Non external object.

  • Pour les paramètres radius = 10& PI = 3.14, nous aurons toujours le même résultat:314.0
  • Pour les paramètres radius = 10& PI = 42, nous aurons toujours le même résultat:4200

Lecture de fichiers

Si notre fonction lit des fichiers externes, ce n'est pas une fonction pure - le contenu du fichier peut changer.

Génération de nombres aléatoires

Toute fonction qui repose sur un générateur de nombres aléatoires ne peut pas être pure.

Il ne provoque aucun effet secondaire observable

Des exemples d'effets secondaires observables incluent la modification d'un objet global ou d'un paramètre passé par référence.

Nous voulons maintenant implémenter une fonction pour recevoir une valeur entière et renvoyer la valeur augmentée de 1.

Nous avons la countervaleur. Notre fonction impure reçoit cette valeur et réassigne le compteur avec la valeur augmentée de 1.

Observation : la mutabilité est déconseillée en programmation fonctionnelle.

Nous modifions l'objet global. Mais comment y arriverions-nous pure? Renvoyez simplement la valeur augmentée de 1. C'est aussi simple que cela.

Voyez que notre fonction pure increase-counterrenvoie 2, mais la countervaleur est toujours la même. La fonction renvoie la valeur incrémentée sans modifier la valeur de la variable.

Si nous suivons ces deux règles simples, il devient plus facile de comprendre nos programmes. Désormais, chaque fonction est isolée et incapable d'avoir un impact sur d'autres parties de notre système.

Les fonctions pures sont stables, cohérentes et prévisibles. Étant donné les mêmes paramètres, les fonctions pures renverront toujours le même résultat. Nous n'avons pas besoin de penser à des situations où le même paramètre a des résultats différents - car cela ne se produira jamais.

Avantages des fonctions pures

Le code est nettement plus facile à tester. Nous n'avons pas besoin de nous moquer de quoi que ce soit. Nous pouvons donc tester des fonctions pures dans différents contextes:

  • Étant donné un paramètre A→ attendez-vous à ce que la fonction renvoie une valeurB
  • Étant donné un paramètre C→ attendez-vous à ce que la fonction renvoie une valeurD

Un exemple simple serait une fonction pour recevoir une collection de nombres et s'attendre à ce qu'elle incrémente chaque élément de cette collection.

Nous recevons la numberscollection, utilisons mapavec la incfonction pour incrémenter chaque nombre, et retournons une nouvelle liste de nombres incrémentés.

Pour le input[1 2 3 4 5], l'attendu outputserait [2 3 4 5 6].

Immutabilité

Inchangé dans le temps ou impossible à modifier.

Lorsque les données sont immuables, leur état ne peut pas changeraprès sa création. Si vous souhaitez modifier un objet immuable, vous ne pouvez pas. Au lieu de cela, vous créez un nouvel objet avec la nouvelle valeur.

En Javascript, nous utilisons couramment la forboucle. Cette forinstruction suivante a quelques variables mutables.

Pour chaque itération, nous modifions le iet l' sumOfValueétat . Mais comment gérer la mutabilité dans l'itération? Récursivité! De retour à Clojure!

Nous avons donc ici la sumfonction qui reçoit un vecteur de valeurs numériques. Le recurretourne dans le loopjusqu'à ce que nous obtenions le vecteur vide (notre récursivité base case). Pour chaque "itération", nous ajouterons la valeur à l' totalaccumulateur.

Avec la récursivité, nous gardons nos variablesimmuable.

Observation : Oui! Nous pouvons utiliser reducepour implémenter cette fonction. Nous verrons cela dans le Higher Order Functionssujet.

Il est également très courant de construire l' état final d'un objet. Imaginons que nous ayons une chaîne et que nous voulions transformer cette chaîne en un fichier url slug.

En POO en Ruby, nous créerions une classe, disons que, UrlSlugify. Et cette classe aura une slugify!méthode pour transformer l'entrée de chaîne en un fichier url slug.

Magnifique! C'est implémenté! Ici, nous avons une programmation impérative disant exactement ce que nous voulons faire dans chaque slugifyprocessus - d'abord en minuscules, puis en supprimant les espaces blancs inutiles et, enfin, en remplaçant les espaces blancs restants par des tirets.

Mais nous modifions l'état d'entrée dans ce processus.

Nous pouvons gérer cette mutation en faisant de la composition de fonctions ou du chaînage de fonctions. En d'autres termes, le résultat d'une fonction sera utilisé comme entrée pour la fonction suivante, sans modifier la chaîne d'entrée d'origine.

Ici nous avons:

  • trim: supprime les espaces aux deux extrémités d'une chaîne
  • lower-case: convertit la chaîne en minuscules
  • replace: remplace toutes les instances de correspondance par remplacement dans une chaîne donnée

Nous combinons les trois fonctions et nous pouvons "slugify"notre chaîne.

En parlant de combinaison de fonctions , nous pouvons utiliser la compfonction pour composer les trois fonctions. Nous allons jeter un coup d'oeil:

Transparence référentielle

Implémentons un square function:

Cette fonction (pure) aura toujours la même sortie, avec la même entrée.

Passer «2» comme paramètre de la square functionretournera toujours 4. Nous pouvons maintenant remplacer le (square 2)par 4. C'est tout! Notre fonction est referentially transparent.

Fondamentalement, si une fonction produit systématiquement le même résultat pour la même entrée, elle est référentiellement transparente.

fonctions pures + données immuables = transparence référentielle

Avec ce concept, une chose intéressante que nous pouvons faire est de mémoriser la fonction. Imaginez que nous ayons cette fonction:

Les (+ 5 8)égaux 13. Cette fonction aboutira toujours à 13. Nous pouvons donc faire ceci:

Et cette expression aboutira toujours à 16. Nous pouvons remplacer l'expression entière par une constante numérique et la mémoriser.

Fonctionne comme des entités de première classe

L'idée des fonctions comme des entités de premier ordre est que les fonctions sont également traitées comme des valeurs et utilisées comme des données.

Dans Clojure, il est courant d'utiliser defnpour définir des fonctions, mais ce n'est que du sucre syntaxique pour (def foo (fn ...)). fnrenvoie la fonction elle-même. defnrenvoie a varqui pointe vers un objet fonction.

Les fonctions en tant qu'entités de première classe peuvent:

  • y faire référence à partir de constantes et de variables
  • le passer en paramètre à d'autres fonctions
  • le renvoyer comme résultat d'autres fonctions

L'idée est de traiter les fonctions comme des valeurs et de transmettre des fonctions comme des données. De cette façon, nous pouvons combiner différentes fonctions pour créer de nouvelles fonctions avec un nouveau comportement.

Imaginez que nous ayons une fonction qui additionne deux valeurs puis double la valeur. Quelque chose comme ça:

Maintenant une fonction qui soustrait des valeurs et renvoie le double:

Ces fonctions ont une logique similaire, mais la différence réside dans les fonctions des opérateurs. Si nous pouvons traiter les fonctions comme des valeurs et les transmettre comme arguments, nous pouvons construire une fonction qui reçoit la fonction opérateur et l'utiliser dans notre fonction. Construisons-le!

Terminé! Maintenant nous avons un fargument, et l'utilisons pour traiter aet b. Nous avons passé les fonctions +et -pour composer avec la double-operatorfonction et créer un nouveau comportement.

Fonctions d'ordre supérieur

Lorsque nous parlons de fonctions d'ordre supérieur, nous entendons une fonction qui soit:

  • prend une ou plusieurs fonctions comme arguments, ou
  • renvoie une fonction comme résultat

La double-operatorfonction que nous avons implémentée ci-dessus est une fonction d'ordre supérieur car elle prend une fonction d'opérateur comme argument et l'utilise.

A propos de Vous avez probablement déjà entendu filter, mapet reduce. Jetons un coup d'œil à ceux-ci.

Filtre

Étant donné une collection, nous voulons filtrer par un attribut. La fonction de filtre attend une valeur trueou falsepour déterminer si l'élément doit ou non être inclus dans la collection de résultats. Fondamentalement, si l'expression de rappel est true, la fonction de filtre inclura l'élément dans la collection de résultats. Sinon, ce ne sera pas le cas.

Un exemple simple est lorsque nous avons une collection d'entiers et que nous ne voulons que les nombres pairs.

Approche impérative

Une manière impérative de le faire avec Javascript est de:

  • créer un vecteur vide evenNumbers
  • itérer sur le numbersvecteur
  • pousser les nombres pairs vers le evenNumbersvecteur

Nous pouvons utiliser la filterfonction d'ordre supérieur pour recevoir la even?fonction et renvoyer une liste de nombres pairs:

Un problème intéressant que j'ai résolu sur Hacker Rank FP Path était le problème Filter Array . L'idée problématique est de filtrer un tableau donné d'entiers et de ne sortir que les valeurs inférieures à une valeur spécifiée X.

Une solution Javascript impérative à ce problème est quelque chose comme:

Nous disons exactement ce que notre fonction doit faire - itérer sur la collection, comparer l'élément actuel de la collection avec x, et pousser cet élément sur resultArrays'il passe la condition.

Approche déclarative

Mais nous voulons une manière plus déclarative de résoudre ce problème, et en utilisant également la filterfonction d'ordre supérieur.

Une solution déclarative de Clojure serait quelque chose comme ceci:

Cette syntaxe semble un peu étrange en premier lieu, mais elle est facile à comprendre.

#(> x%) est juste une fonction anonyme qui reçoit es x et la compare à chaque élément de la collectio n. % représente le paramètre de la fonction anonyme - dans ce cas, l'élément courant à l'intérieur de t he filter.

Nous pouvons également le faire avec des cartes. Imaginez que nous ayons une carte des personnes avec leur nameet age. Et nous voulons filtrer uniquement les personnes ayant une valeur d'âge spécifiée, dans cet exemple les personnes âgées de plus de 21 ans.

Résumé du code:

  • nous avons une liste de personnes (avec nameet age).
  • nous avons la fonction anonyme #(< 21 (:age %)). Rappelez-vous que t he% représente l'élément actuel de la collection? Eh bien, l'élément de la collection est une carte des personnes. Si nous do (:age {:name "TK" :age 26}), il renvoie la valeur d'âge e,26 dans ce cas.
  • nous filtrons toutes les personnes en fonction de cette fonction anonyme.

Carte

L'idée de la carte est de transformer une collection.

La mapméthode transforme une collection en appliquant une fonction à tous ses éléments et en créant une nouvelle collection à partir des valeurs renvoyées.

Obtenons la même peoplecollection ci-dessus. Nous ne voulons pas filtrer par «sur l'âge» maintenant. Nous voulons juste une liste de chaînes, quelque chose comme TK is 26 years old. Ainsi, la chaîne finale pourrait être :name is :age years old:nameet :agesont les attributs de chaque élément de la peoplecollection.

De manière Javascript impérative, ce serait:

De manière déclarative Clojure, ce serait:

L'idée est de transformer une collection donnée en une nouvelle collection.

Un autre problème intéressant de Hacker Rank était le problème de la liste de mise à jour . Nous voulons simplement mettre à jour les valeurs d'une collection donnée avec leurs valeurs absolues.

Par exemple, l'entrée a [1 2 3 -4 5]besoin que la sortie soit [1 2 3 4 5]. La valeur absolue de -4est 4.

Une solution simple serait une mise à jour sur place pour chaque valeur de collection.

Nous utilisons la Math.absfonction pour transformer la valeur en sa valeur absolue et effectuer la mise à jour sur place.

Ce n'est pas une manière fonctionnelle de mettre en œuvre cette solution.

Tout d'abord, nous avons appris l'immuabilité. Nous savons à quel point l'immuabilité est importante pour rendre nos fonctions plus cohérentes et prévisibles. L'idée est de construire une nouvelle collection avec toutes les valeurs absolues.

Deuxièmement, pourquoi ne pas utiliser mapici pour «transformer» toutes les données?

Ma première idée a été de créer une to-absolutefonction pour gérer une seule valeur.

S'il est négatif, nous voulons le transformer en une valeur positive (la valeur absolue). Sinon, nous n'avons pas besoin de le transformer.

Maintenant que nous savons comment faire absolutepour une valeur, nous pouvons utiliser cette fonction pour passer en argument à la mapfonction. Vous souvenez-vous que a higher order functionpeut recevoir une fonction comme argument et l'utiliser? Oui, la carte peut le faire!

Sensationnel. Si belle! ?

Réduire

L'idée de réduire est de recevoir une fonction et une collection, et de renvoyer une valeur créée en combinant les éléments.

Un exemple courant dont les gens parlent est d'obtenir le montant total d'une commande. Imaginez que vous vous trouviez sur un site Web d'achat. Vous avez ajouté Product 1, Product 2, Product 3et Product 4à votre panier (commande). Nous voulons maintenant calculer le montant total du panier.

De manière impérative, nous itérerions la liste des commandes et additionnerions le montant de chaque produit au montant total.

En utilisant reduce, nous pouvons créer une fonction pour gérer le amount sumet le passer comme argument à la reducefonction.

Ici, nous avons shopping-cart, la fonction sum-amountqui reçoit le courant total-amountet l' current-productobjet pour sumeux.

La get-total-amountfonction est utilisée à reducel' shopping-cartaide de sum-amountet à partir de 0.

Une autre façon d'obtenir le montant total est de composer mapet reduce. Qu'est-ce que je veux dire par là? Nous pouvons utiliser mappour transformer le shopping-carten une collection de amountvaleurs, puis utiliser simplement la reducefonction avec +function.

Le get-amountreçoit l'objet produit et renvoie uniquement la amountvaleur. Donc ce que nous avons ici est [10 30 20 60]. Et puis le reducecombine tous les éléments en additionnant. Magnifique!

Nous avons examiné le fonctionnement de chaque fonction d'ordre supérieur. Je veux vous montrer un exemple de la façon dont nous pouvons composer les trois fonctions dans un exemple simple.

En parlant shopping cart, imaginez que nous avons cette liste de produits dans notre commande:

Nous voulons le montant total de tous les livres dans notre panier. Aussi simple que cela. L'algorithme?

  • filtrer par type de livre
  • transformer le panier en une collection de montant à l'aide de map
  • combiner tous les éléments en les additionnant avec réduire

Terminé! ?

Ressources

J'ai organisé quelques ressources que j'ai lues et étudiées. Je partage celles que j'ai trouvées vraiment intéressantes. Pour plus de ressources, visitez mon référentiel Github de programmation fonctionnelle .

  • Ressources spécifiques à Ruby
  • Ressources spécifiques à Javascript
  • Clojure des ressources spécifiques

Intros

  • Apprendre la PF dans JS
  • Introduction à FP avec Python
  • Vue d'ensemble de la PF
  • Une introduction rapide au JS fonctionnel
  • Qu'est-ce que la PF?
  • Jargon de programmation fonctionnelle

Fonctions pures

  • Qu'est-ce qu'une fonction pure?
  • Programmation fonctionnelle pure 1
  • Programmation fonctionnelle pure 2

Données immuables

  • DS immuable pour la programmation fonctionnelle
  • Pourquoi l'état mutable partagé est la racine de tout mal
  • Partage structurel dans Clojure: Partie 1
  • Partage structurel dans Clojure: partie 2
  • Partage structurel dans Clojure: Partie 3
  • Partage structurel dans Clojure: partie finale

Fonctions d'ordre supérieur

  • Eloquent JS: fonctions d'ordre supérieur
  • Fonction amusante et amusante Filtre
  • Carte des fonctions amusantes et amusantes
  • Fonction amusante et amusante Basic
  • Fonction amusante et amusante Avancé Réduire
  • Fonctions d'ordre supérieur de Clojure
  • Filtre purement fonctionnel
  • Carte purement fonctionnelle
  • Réduire purement fonctionnel

Programmation déclarative

  • Programmation déclarative vs impérative

C'est ça!

Salut les gens, j'espère que vous vous êtes amusé à lire cet article, et j'espère que vous avez beaucoup appris ici! C'était ma tentative de partager ce que j'apprends.

Voici le référentiel avec tous les codes de cet article.

Venez apprendre avec moi. Je partage des ressources et mon code dans ce référentiel de programmation fonctionnelle d'apprentissage .

J'espère que vous avez vu quelque chose d'utile ici. Et à la prochaine fois! :)

Mon Twitter et Github. ☺

TK.