Introduction pratique à la programmation fonctionnelle

Tutoriel pour apprendre la programmation fonctionnelle en Python

Mary Rose Cook

Traducteur

Laurent Rosenfeld

De nombreux articles sur la programmation fonctionnelle enseignent les techniques fonctionnelles abstraites telles que la composition, la programmation en pipeline, les fonctions d'ordre supérieur, etc.

Ce tutoriel est différent : il donne des exemples de code écrits en programmation impérative ou procédurale non fonctionnelle comme beaucoup en écrivent tous les jours, puis il traduit ces exemples en programmation de style fonctionnel.

Commentez

- Titre : Introduction pratique à la programmation fonctionnelle

- Auteur : Mary Rose Cook

- Traducteur : Laurent Rosenfeld

- Parution : 14 mars 2017

- Mise à jour : 14 mars 2017

- Public visé :

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2017 Mary Rose Cook. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Voir également :

- Pipelines de données en Perl

- Fonctions d'ordre supérieur et fermetures en Perl

- Versione étendues de map et de grep, reduce, curryfication en Perl

Sommaire

- 1. Introduction

- 2. Le fil conducteur

- 3. N'itérez pas sur des listes : utilisez map et reduce

- 3-1. Map

- 3-2. Reduce

- 4. Écrivez du code déclaratif et non impératif

- 4-1. Utilisez des fonctions

- 4-2. Suppression des états globaux

- 5. Utilisation de pipelines

- 6. Et maintenant ?

- 7. Remerciements

1 - Introduction

La première partie de ce tutoriel prend des exemples simples de boucles de transformation de données et les traduit en instructions de type map ou reduce. La seconde partie considère des boucles plus complexes, les scinde en fragments unitaires et rend chacun de ces fragments fonctionnel. La troisième partie prend en exemple une longue boucle consistant en une série de transformations successives des données et la décompose en un pipeline fonctionnel.

Les exemples sont écrits en Python, car beaucoup de personnes trouvent le code Python facile à lire. Beaucoup de ces exemples évitent les particularités propres à Python afin d'illustrer des techniques qui sont communes à beaucoup de langages de programmation : map, reduce, programmation par flux de données ou en pipeline. Tous les exemples de code sont écrits en Python 2.

2 - Le fil conducteur

Quand on aborde la programmation fonctionnelle, c'est souvent pour parler d'un nombre étourdissant de caractéristiques « fonctionnelles » : les données immuables(1), les fonctions de première classe(2) et l'optimisation de la récursion terminale(3). Ces fonctionnalités ne sont que des caractéristiques de langage qui facilitent la programmation fonctionnelle. On parle aussi de mappage, de réduction, de pipeline, de récursion, de curryfication(4) et d'utilisation des fonctions d'ordre supérieur. Ce sont des techniques de programmation employées pour écrire du code fonctionnel. Il est enfin question de parallélisation(5), d'évaluation paresseuse(6) et de déterminisme(7). Ce ne sont que des propriétés avantageuses des programmes fonctionnels.

Oubliez tout cela. Un programme écrit en style fonctionnel se caractérise essentiellement par une chose : l'absence d'effets de bord. Le code ne dépend pas de données se trouvant à l'extérieur de la fonction courante et il ne modifie pas des données à l'extérieur de cette fonction. Toutes les autres caractéristiques de la programmation fonctionnelle peuvent se déduire de cette propriété. Utilisez-la comme un fil conducteur lors de votre apprentissage.

Voici un exemple de fonction non fonctionnelle :

1. a = 0

2. def increment():

3. global a

4. a += 1

Et voici une fonction fonctionnelle :

1. def increment(a):

2. return a + 1

3 - N'itérez pas sur des listes : utilisez map et reduce

3-1 - Map

La fonction map prend en argument une fonction et une collection de données. Elle crée une nouvelle collection vide, applique la fonction à chaque élément de la collection d'origine et insère les valeurs de retour produites dans la nouvelle collection. Finalement, elle renvoie la nouvelle collection.

Voici un map simple qui prend en entrée une liste de noms et renvoie une liste contenant la longueur de chacun de ces noms :

1. name_lengths = map(len, ["Mary", "Isla", "Sam"])

3. print name_lengths

4. # => [4, 4, 3]

Ce map élève au carré chacun des nombres de la collection qui lui est passée :

1. squares = map(lambda x: x * x, [0, 1, 2, 3, 4])

3. print squares

4. # => [0, 1, 4, 9, 16]

Ce map ne prend pas une fonction nommée en paramètre : il prend une fonction anonyme en ligne définie à l'aide du mot-clef lambda. Les paramètres de cette fonction lambda sont définis à gauche du caractère deux-points et le corps de la fonction est défini à sa droite. Le résultat de l'exécution du corps de cette fonction est renvoyé (implicitement).

Le code non fonctionnel ci-dessous prend une liste de noms réels et les remplace par des noms de code choisis aléatoirement.

1. import random

3. names = ['Mary', 'Isla', 'Sam']

4. code_names = ['Mr. Pink', 'Mr. Orange', 'Mr. Blonde']

6. for i in range(len(names)):

7. names[i] = random.choice(code_names)

9. print names

10. # => ['Mr. Blonde', 'Mr. Blonde', 'Mr. Blonde']

(Comme vous pouvez le remarquer, cet algorithme peut éventuellement affecter le même nom de code secret à plusieurs des agents secrets. Espérons que cela ne constituera pas une source de confusion au cours de la mission secrète.)

Nous pouvons réécrire ce code avec un map :

1. import random

3. names = ['Mary', 'Isla', 'Sam']

5. secret_names = map(lambda x: random.choice(['Mr. Pink',

6. 'Mr. Orange',

7. 'Mr. Blonde']),

8. names)

Exercice 1 : essayez de réécrire sous la forme d'un map le code ci-dessous, qui prend en entrée une liste de noms réels et les remplace par des noms de code produits à l'aide d'une stratégie plus robuste :

1. names = ['Mary', 'Isla', 'Sam']

3. for i in range(len(names)):

4. names[i] = hash(names[i])

6. print names

7. # => [6306819796133686941, 8135353348168144921, -1228887169324443034]

(Espérons que les agents secrets ont une bonne mémoire et n'oublieront pas le nom de code de leurs collègues au cours de leur mission secrète.)

Voici ma solution :

1. names = ['Mary', 'Isla', 'Sam']

3. secret_names = map(hash, names)

3-2 - Reduce

La fonction reduce prend en entrée une fonction et une collection d'éléments. Elle renvoie une valeur créée en combinant les éléments de la collection.

Voici une réduction simple. Elle renvoie la somme de tous les éléments de la collection :

1. sum = reduce(lambda a, x: a + x, [0, 1, 2, 3, 4])

3. print sum

4. # => 10

x est l'élément courant de l'itération et a est l'accumulateur. C'est la valeur renvoyée par l'exécution de la fonction lambda sur l'élément précédent. La fonction reduce() parcourt les éléments de la liste et, pour chacun d'eux, exécute la lambda sur les valeurs courantes de a et de x et renvoie le résultat qui devient le a de l'itération suivante.

Que vaut a lors de la première itération ? Il n'y a pas de résultat d'une itération précédente à lui passer. La fonction reduce() utilise la première valeur de la liste pour a, et commence à itérer à partir de la seconde valeur. Autrement dit, la première valeur de x est le second élément de la liste.

Le programme suivant compte le nombre d'occurrences du mot « Sam » dans une liste de chaînes de caractères :

1. sentences = ['Mary read a story to Sam and Isla.',

2. 'Isla cuddled Sam.',

3. 'Sam chortled.']

5. sam_count = 0

6. for sentence in sentences:

7. sam_count += sentence.count('Sam')

9. print sam_count

10. # => 3

Voici le même programme réécrit avec un reduce :

1. sentences = ['Mary read a story to Sam and Isla.',

2. 'Isla cuddled Sam.',

3. 'Sam chortled.']

5. sam_count = reduce(lambda a, x: a + x.count('Sam'),

6. sentences,

7. 0)

Comment ce programme détermine-t-il la valeur initiale de a ? La valeur de départ pour le nombre d'occurrences de « Sam » ne peut pas être la chaîne de caractères « Mary read a story to Sam and Isla. » La valeur initiale de l'accumulateur est spécifiée dans le troisième argument de la fonction reduce(). Ce mécanisme permet d'utiliser une valeur de départ d'un type différent de celui des valeurs de la collection fournie en entrée.

Pourquoi les fonctions map et reduce sont-elles meilleures ?

Premièrement, le code est plus court et tient souvent en une seule ligne.

Deuxièmement, les parties importantes de l'itération - la collection, l'opération et la valeur de retour - figurent au même endroit dans tous les map et reduce.

Troisièmement, le code d'une boucle peut affecter des variables définies avant la boucle ou le code situé après. Par convention, map et reduce sont fonctionnels (et n'ont donc pas d'effets de bord).

Quatrièmement, map et reduce sont des opérations élémentaires. Chaque fois qu'on lit une boucle for, il faut analyser la logique ligne par ligne. Il existe des éléments récurrents que l'on peut utiliser pour construire un échafaudage permettant d'asseoir la compréhension du code. En revanche, map et reduce constituent immédiatement des briques de construction qui peuvent se combiner pour assembler des algorithmes complexes. Et ce sont des éléments que celui qui lit le code peut comprendre instantanément et abstraire dans son esprit : « Ah, dira-t-il peut-être, cette ligne de code transforme chaque élément de la collection, puis met à la poubelle certains des éléments ainsi transformés. Et il combine le reste pour n'en faire qu'un seul résultat. »

Cinquièmement, les fonctions map et reduce ont de nombreuses amies qui offrent des fonctionnalités modifiées et utiles, par exemple : filter, all, any et find.

Exercice 2 : essayez de réécrire le code ci-dessous en utilisant map, reduce et filter. La fonction filter prend en entrée une fonction et une collection. Elle renvoie une nouvelle collection contenant tous les éléments de la collection d'origine pour laquelle la fonction renvoie une valeur vraie (True).

1. people = [{'name': 'Mary', 'height': 160},

2. {'name': 'Isla', 'height': 80},

3. {'name': 'Sam'}]

5. height_total = 0

6. height_count = 0

7. for person in people:

8. if 'height' in person:

9. height_total += person['height']

10. height_count += 1

12. if height_count > 0:

13. average_height = height_total / height_count

15. print average_height

16. # => 120

Si cela vous paraît semé d'embûches, essayez de ne pas penser du point de vue des opérations sur les données. Pensez plutôt aux états que les données vont connaître, depuis les dictionnaires de personnes au départ jusqu'à la taille moyenne de ces personnes à la fin. N'essayez pas de regrouper des transformations multiples. Mettez chacune de ces transformations dans une ligne distincte et affectez le résultat à une variable ayant un nom descriptif. Une fois que le code fonctionne, vous pouvez songer à le condenser.

Voici ma solution :

1. people = [{'name': 'Mary', 'height': 160},

2. {'name': 'Isla', 'height': 80},

3. {'name': 'Sam'}]

5. heights = map(lambda x: x['height'],

6. filter(lambda x: 'height' in x, people))

8. if len(heights) > 0:

9. from operator import add

10. average_height = reduce(add, heights) / len(heights)

4 - Écrivez du code déclaratif et non impératif

Le programme ci-dessous organise une course entre trois automobiles. À chaque instant, chacune des voitures peut avancer ou rester arrêtée. À chaque fois, le programme affiche la trajectoire des automobiles jusqu'à présent. Après cinq étapes, la course s'arrête.

Voici d'abord l'affichage d'un résultat possible :

-
--
--

--
--
---

---
--
---

----
---
----

----
----
-----

Et voici le programme :

1. from random import random

3. time = 5

4. car_positions = [1, 1, 1]

6. while time:

7. # decrease time

8. time -= 1

10. print ''

11. for i in range(len(car_positions)):

12. # move car

13. if random() > 0.3:

14. car_positions[i] += 1

16. # draw car

17. print '-' * car_positions[i]

Ce programme est écrit en style impératif. Une version fonctionnelle serait déclarative. Elle décrirait ce qu'il faut faire et non comment le faire.

4-1 - Utilisez des fonctions

Un programme peut être rendu plus déclaratif en assemblant des bouts de code dans des fonctions.

1. from random import random

3. def move_cars():

4. for i, _ in enumerate(car_positions):

5. if random() > 0.3:

6. car_positions[i] += 1

8. def draw_car(car_position):

9. print '-' * car_position

11. def run_step_of_race():

12. global time

13. time -= 1

14. move_cars()

16. def draw():

17. print ''

18. for car_position in car_positions:

19. draw_car(car_position)

21. time = 5

22. car_positions = [1, 1, 1]

24. while time:

25. run_step_of_race()

26. draw()

Pour comprendre ce programme, le lecteur lit simplement la boucle principale : « S'il y a encore du temps, exécute une étape de la course et dessine. Puis vérifie à nouveau s'il reste du temps. » Si le lecteur désire savoir ce que signifie exécuter une étape ou dessiner, il lui suffit de lire ces fonctions.

Les commentaires deviennent inutiles. Le code s'autodocumente.

Répartir le code en fonctions est une excellente façon de rendre le code plus lisible sans demander de gros efforts intellectuels.

Cette technique utilise des fonctions, mais il les utilise comme ce que l'on appelle parfois des sous-routines ou des procédures. Ces fonctions regroupent des lignes de code. Mais ce code n'est pas fonctionnel au sens du fil conducteur. Ces fonctions utilisent des états qui ne lui ont pas été passés sous la forme d'arguments. Elles affectent le code aux alentours en modifiant des variables externes au lieu de renvoyer des valeurs. Pour comprendre ce qu'une fonction fait réellement, le lecteur doit lire soigneusement chaque ligne de code. S'il rencontre une variable externe, il doit en trouver l'origine. Il doit vérifier si d'autres fonctions n'affectent pas cette variable.

4-2 - Suppression des états globaux

Voici le code d'une version fonctionnelle de la course automobile :

1. from random import random

3. def move_cars(car_positions):

4. return map(lambda x: x + 1 if random() > 0.3 else x,

5. car_positions)

7. def output_car(car_position):

8. return '-' * car_position

10. def run_step_of_race(state):

11. return {'time': state['time'] - 1,

12. 'car_positions': move_cars(state['car_positions'])}

14. def draw(state):

15. print ''

16. print '\n'.join(map(output_car, state['car_positions']))

18. def race(state):

19. draw(state)

20. if state['time']:

21. race(run_step_of_race(state))

23. race({'time': 5,

24. 'car_positions': [1, 1, 1]})

Le code est toujours réparti en fonctions, mais ces fonctions sont… fonctionnelles. Trois symptômes le montrent. D'abord, il n'y a plus de variables partagées (global). Les temps et les positions des véhicules sont passés en paramètres à la fonction race(). Ensuite, les fonctions admettent des paramètres. Enfin, il n'y a pas de variables instanciées à l'intérieur des fonctions. Toutes les modifications de données s'effectuent sous la forme de valeurs de retour. La fonction race() s'appelle récursivement avec le résultat de run_step_of_race(). Chaque fois qu'une étape génère un nouvel état, celui-ci est immédiatement passé à l'étape suivante.

Considérons maintenant deux fonctions, zero() et one().

1. def zero(s):

2. if s[0] == "0":

3. return s[1:]

5. def one(s):

6. if s[0] == "1":

7. return s[1:]

La fonction zero() prend en paramètre une chaîne de caractères : s. Si le premier caractère est un « 0 », elle renvoie le reste de la chaîne ; dans le cas contraire, elle renvoie None. La fonction one() fait la même chose, mais pour un premier caractère égal à « 1 ».

Imaginons maintenant une fonction nommée rule_sequence(), qui prend en entrée une chaîne de caractères et une liste de fonctions-règles de la même forme que zero() et one(). Elle appelle la première règle sur la chaîne et appelle la seconde règle en lui passant en paramètre la valeur de retour de la première règle, sauf si cette valeur est None. Elle passe ensuite en paramètre à la troisième règle la valeur renvoyée par la seconde, sauf s'il s'agit de None, et ainsi de suite. Si l'une quelconque des règles renvoie None, rule_sequence() s'arrête et renvoie None. Sinon, elle renvoie la valeur de retour de la dernière règle.

Voici un exemple de données en entrée et en sortie :

1. print rule_sequence('0101', [zero, one, zero])

2. # => 1

4. print rule_sequence('0101', [zero, zero])

5. # => None

Voici une version impérative de rule_sequence() :

1. def rule_sequence(s, rules):

2. for rule in rules:

3. s = rule(s)

4. if s == None:

5. break

7. return s

Exercice 3 : le programme ci-dessus utilise une boucle pour faire le travail. Rendez-le plus déclaratif en le réécrivant sous une forme récursive.

Voici ma solution :

1. def rule_sequence(s, rules):

2. if s == None or not rules:

3. return s

4. else:

5. return rule_sequence(rules[0](s), rules[1:])

5 - Utilisation de pipelines

Dans la section précédente, nous avons réécrit des boucles impératives sous la forme de récursions appelant éventuellement des fonctions auxiliaires. Ici nous allons réécrire un type différent de boucle impérative à l'aide d'une technique nommée pipeline.

La boucle ci-dessous effectue des transformations sur des dictionnaires qui contiennent le nom, le pays d'origine erroné et le statut (actif ou inactif) de groupes musicaux.

1. bands = [{'name': 'sunset rubdown', 'country': 'UK', 'active': False},

2. {'name': 'women', 'country': 'Germany', 'active': False},

3. {'name': 'a silver mt. zion', 'country': 'Spain', 'active': True}]

5. def format_bands(bands):

6. for band in bands:

7. band['country'] = 'Canada'

8. band['name'] = band['name'].replace('.', '')

9. band['name'] = band['name'].title()

11. format_bands(bands)

13. print bands

14. # => [{'name': 'Sunset Rubdown', 'active': False, 'country': 'Canada'},

15. #     {'name': 'Women', 'active': False, 'country': 'Canada' },

16. #     {'name': 'A Silver Mt Zion', 'active': True, 'country': 'Canada'}]

Le nom de la fonction peut susciter quelque inquiétude. « format » est très vague. Un examen plus attentif du code ne peut que renforcer cette inquiétude. Il se passe trois choses dans la même boucle. Le « pays » est changé en « Canada ». Les signes de ponctuation sont supprimés du nom du groupe, et le nom du groupe est passé en lettres capitales. Il est difficile de dire ce que le code est censé faire, et difficile de dire s'il fait bien ce qu'il paraît faire. Le code est difficile à réutiliser, à tester et à paralléliser.

Comparez avec le code suivant :

1. print pipeline_each(bands, [set_canada_as_country,

2. strip_punctuation_from_name,

3. capitalize_names])

Ce code est facile à comprendre. Il donne l'impression que les fonctions auxiliaires sont fonctionnelles puisqu'elles paraissent chaînées : le résultat d'une fonction devient la donnée en entrée de la suivante. Si elles sont fonctionnelles, elles sont faciles à vérifier et elles sont également faciles à réutiliser, à tester et à paralléliser.

Le but de pipeline_each() est de passer en paramètre les groupes, un à la fois, à une fonction de transformation comme set_canada_as_country(). Une fois cette transformation appliquée à chacun des groupes, la fonction pipeline_each() crée une nouvelle liste de groupes transformée. Elle passe alors chacun de ces groupes à la fonction suivante.

Jetons un coup d'œil aux fonctions de transformation :

1. def assoc(_d, key, value):

2. from copy import deepcopy

3. d = deepcopy(_d)

4. d[key] = value

5. return d

7. def set_canada_as_country(band):

8. return assoc(band, 'country', "Canada")

10. def strip_punctuation_from_name(band):

11. return assoc(band, 'name', band['name'].replace('.', ''))

13. def capitalize_names(band):

14. return assoc(band, 'name', band['name'].title())

Chacune d'entre elles associe une nouvelle valeur à une clef d'un groupe. Ce n'est pas facile à réaliser sans modifier le groupe d'origine. La fonction assoc() résout ce problème en utilisant la fonction deepcopy() pour produire une copie du dictionnaire reçu en paramètre. Chaque fonction de transformation effectue sa modification sur cette copie et renvoie cette copie.

Tout cela semble fonctionner. Les dictionnaires d'origine sont protégés contre la modification quand une nouvelle valeur est affectée à une clef, mais il y a potentiellement deux autres modifications dans le code ci-dessus. Dans strip_punctuation_from_name(), le nom sans ponctuation est généré en appelant replace() sur le nom d'origine. De même, dans capitalize_names(), le nouveau nom en majuscules est généré en appelant title() sur le nom d'origine. Si les fonctions replace() et title() ne sont pas fonctionnelles, alors strip_punctuation_from_name() et replace() ne le sont pas non plus.

Heureusement, il se trouve que les fonctions internes replace() et title() ne modifient pas les chaînes de caractères qui leur sont passées en arguments, parce que les chaînes sont immuables en Python. Par exemple, quand replace() traite le nom d'un groupe, le nom du groupe d'origine est copié et replace() agit sur cette copie. Ouf !

Cette différence de mutabilité entre les chaînes de caractères et les dictionnaires illustre l'intérêt de langages de programmation comme Clojure : le programmeur n'a jamais besoin de se demander si les données sont mutables ou non, elles ne le sont pas.

Exercice 4 : essayez d'écrire la fonction pipeline_each. Pensez à l'ordre des opérations. Les groupes du tableau bands sont passés, un par un, à la première fonction de transformation. Les groupes du tableau résultant sont ensuite passés, un à la fois, à la seconde fonction, et ainsi de suite.

Voici ma solution :

1. def pipeline_each(data, fns):

2. return reduce(lambda a, x: map(x, a),

3. fns,

4. data)

Chacune des trois fonctions de transformation se résume à la modification d'un champ particulier du groupe passé en argument. La fonction call() permet d'en faire une fonction abstraite. Elle reçoit en paramètre la fonction à appliquer et la clef de la valeur à traiter.

1. set_canada_as_country = call(lambda x: 'Canada', 'country')

2. strip_punctuation_from_name = call(lambda x: x.replace('.', ''), 'name')

3. capitalize_names = call(str.title, 'name')

5. print pipeline_each(bands, [set_canada_as_country,

6. strip_punctuation_from_name,

7. capitalize_names])

Ou, si nous sommes prêts à sacrifier un peu de lisibilité sur l'autel de la concision :

1. print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),

2. call(lambda x: x.replace('.', ''), 'name'),

3. call(str.title, 'name')])

Le code de la fonction call() :

1. def assoc(_d, key, value):

2. from copy import deepcopy

3. d = deepcopy(_d)

4. d[key] = value

5. return d

7. def call(fn, key):

8. def apply_fn(record):

9. return assoc(record, key, fn(record.get(key)))

10. return apply_fn

Il se passe beaucoup de choses ici. Décortiquons morceau par morceau :

1. call() est une fonction d'ordre supérieur. Une fonction d'ordre supérieur est une fonction qui prend une fonction en argument ou renvoie une fonction. Ou qui, comme dans le cas de call(), fait les deux ;

2. apply_fn() ressemble beaucoup aux trois fonctions de transformation. Elle prend en entrée un enregistrement (un groupe) et recherche la valeur de record[key]. Elle appelle fn sur cette valeur, affecte le résultat à une copie de l'enregistrement et renvoie cette copie ;

3. call() ne fait aucun travail véritable. C'est apply_fn() qui effectue le vrai travail une fois appelée. Dans l'exemple d'utilisation de pipeline_each() ci-dessus, une instance de la fonction apply_fn() affectera 'Canada' à 'country'. Une autre instance mettra en lettres capitales le nom du groupe ;

4. Quand une instance de apply_fn() s'exécute, fn et key ne sont pas dans la portée, puisqu'ils ne sont pas passés en paramètres, ni ne sont déclarés localement dans la fonction. Mais ils sont néanmoins accessibles. Quand une fonction est définie, elle sauvegarde des références sur les variables de l'environnement (en dehors de la fonction) où elle est définie et ces références sont utilisées dans la fonction. On dit qu'elle « se ferme » sur ces variables et qu'une telle fonction est une « fermeture ». Quand une fonction s'exécute et que son code réfère à une variable, Python la recherche parmi les variables locales à la fonction et les paramètres reçus lors de l'appel. S'il n'y trouve pas cette variable, alors il la recherche parmi les références sauvegardées sur les variables sur lesquelles la fonction s'est fermée. Et c'est là qu'il trouvera fn et key ;

5. Le code de call() ne fait aucune référence aux groupes, parce que call() pourrait servir à générer des fonctions de type pipeline pour un programme quelconque, quel que soit son objet. La programmation fonctionnelle consiste en partie à assembler une librairie de fonctions génériques, abstraites, réutilisables et composables ;

Bon boulot. Les fermetures, les fonctions d'ordre supérieur et la portée des variables couvertes en l'espace de quelques paragraphes… Allez, faites une pause et prenez un bon verre de limonade.(8)

Il reste un dernier traitement à effectuer sur nos groupes : tout supprimer à l'exception du nom et du pays. La fonction extract_name_and_country() peut extraire cette information :

1. def extract_name_and_country(band):

2. plucked_band = {}

3. plucked_band['name'] = band['name']

4. plucked_band['country'] = band['country']

5. return plucked_band

7. print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),

8. call(lambda x: x.replace('.', ''), 'name'),

9. call(str.title, 'name'),

10. extract_name_and_country])

12. # => [{'name': 'Sunset Rubdown', 'country': 'Canada'},

13. #     {'name': 'Women', 'country': 'Canada'},

14. #     {'name': 'A Silver Mt Zion', 'country': 'Canada'}]

6. La fonction extract_name_and_country() pourrait s'écrire sous la forme de la fonction générique pluck() qui serait utilisée comme suit :

1. print pipeline_each(bands, [call(lambda x: 'Canada', 'country'),

2. call(lambda x: x.replace('.', ''), 'name'),

3. call(str.title, 'name'),

4. pluck(['name', 'country'])])

7. Exercice 5 : pluck() prend en paramètre une liste de clefs à extraire de chaque enregistrement. Essayez de l'écrire. Il est nécessaire que ce soit une fonction d'ordre supérieur.

Voici ma solution :

1. def pluck(keys):

2. def pluck_fn(record):

3. return reduce(lambda a, x: assoc(a, x, record[x]),

4. keys,

5. {})

6. return pluck_fn

6 - Et maintenant ?

Le code fonctionnel coexiste très bien avec du code écrit dans d'autres styles de programmation. Les transformations décrites dans cet article peuvent s'appliquer à une base de code quelconque, presque dans n'importe quel langage de programmation(9). Essayez de les employer dans vos propres programmes.

Pensez à Mary, Isla et Sam. Convertissez vos itérations sur des listes en des map et des reduce.

Pensez à la course automobile. Divisez votre code en fonctions. Rendez ces fonctions fonctionnelles. Transformez une boucle qui se répète en une récursion.

Pensez aux groupes de musiciens. Transformez une suite d'opérations en un pipeline.

7 - Remerciements

Nous remercions Mary Rose Cook de nous avoir autorisés à traduire et republier son billet initialement publié ici : https://maryrosecook.com/blog/post/a-practical-introduction-to-functional-programming.

Nous remercions lolo78 pour la traduction de ce tutoriel, Laethy et genthial pour la relecture.

(1)Une donnée immuable ou non mutable est une donnée qui ne peut pas être modifiée. Dans certains langages fonctionnels comme Clojure, toutes les données sont immuables par défaut. Toute opération de « modification » fait une copie de la valeur, modifie cette valeur et renvoie la copie modifiée. Ceci élimine le risque de bogue dû au fait que le programmeur a un aperçu incomplet de tous les états possibles dans lesquels un programme peut entrer.

(2)Les langages supportant les fonctions de première classe permettent de traiter les fonctions comme n'importe quelle valeur ordinaire. Ceci signifie notamment que l'on peut créer des fonctions, les passer comme arguments à d'autres fonctions, les employer comme valeurs de retour d'autres fonctions et les stocker à l'intérieur de structures de données.

(3)L'optimisation de la récursion terminale est une fonctionnalité d'un langage de programmation. Chaque fois qu'une fonction est appelée récursivement, une nouvelle structure de pile est créée. Cette structure sert à stocker les arguments et les valeurs locales de l'invocation courante de la fonction. Si une fonction s'appelle récursivement un grand nombre de fois, il peut arriver que l'interpréteur ou le compilateur n'ait plus assez de mémoire. Les langages supportant l'optimisation de récursion terminale peuvent dans certains cas réutiliser la même structure de pile pour l'ensemble de la chaîne des appels récursifs. Des langages comme Python qui n'ont pas cette fonctionnalité d'optimisation de la récursion terminale limitent généralement le nombre d'appels récursifs possibles pour une fonction à un plafond de l'ordre de quelques milliers. La fonction race() examinée plus loin dans cet article n'effectue que cinq appels récursifs, il n'y a donc aucun problème.

(4)La curryfication est une technique permettant de décomposer une fonction prenant plusieurs arguments en une fonction prenant le premier argument et renvoyant une autre fonction prenant l'argument suivant, et ainsi de suite pour tous les arguments.

(5)La parallélisation consiste à faire tourner le même code de façon concurrente sans synchronisation. Ces processus concurrents s'exécutent souvent sur des processeurs multiples.

(6)L'évaluation paresseuse est une technique de compilation qui diffère l'exécution de code jusqu'à ce que le résultat soit nécessaire.

(7)Un processus est déterministe s'il renvoie le même résultat chaque fois qu'il est appelé avec les mêmes données en entrée.

(8)Ou, si comme moi vous préférez les grands Médoc, de Château Margaux (mais avec modération). (NdT)

(9)Voir par exemple une série de tutoriels publiés sur ce site sur la programmation fonctionnelle en Perl, dont le premier est à l'adresse suivante  https://laurent-rosenfeld.developpez.com/tutoriels/perl/programmation-fonctionnelle/operateurs-listes/. (NdT)