La Prog Fonctionnelle, Partie III
|
20-09-2014, 00h23
(Modification du message : 20-09-2014, 12h41 par ark.)
Message : #1
|
|
b0fh
Membre actif Messages : 210 Sujets : 17 Points: 309 Inscription : Jul 2012 |
La Prog Fonctionnelle, Partie III
La Prog Fonctionnelle, Partie III
Evaluation Paresseuse Python, ayant une sémantique stricte, évalue complètement ses arguments avant d'appeler le corps de la fonction. Dans un langage paresseux, comme Haskell, le corps d'une fonction peut commencer a s'évaluer avant ses arguments. Dans le cas des listes, il est possible que seul le début d'une liste soit évalué. Dans ce cas, un pipeline de map/filter aura le même comportement que la boucle for, puisque les listes intermédiaires seront évaluées en parallèle, a la demande, pour être détruites aussitôt. Le compilo pourra éliminer les allocations mémoire intermédiaires inutiles. Nous allons simuler la sémantique paresseuse à l'aide de fonctions: pour empêcher l'évaluation d'un argument, nous pouvons l'enrober dans une fonction sans argument. A titre d'exemple, essayons d'écrire une fonction qui émule le comportement d'un operateur ternaire. Code PYTHON :
def my_if(test, good, bad): Cette fonction n'a pas la même sémantique qu'un opérateur ternaire. Voyez-vous pourquoi ? Le code suivant: Code PYTHON :
if 0 == 1: N'est très clairement pas équivalent à Code PYTHON :
my_if(0 == 1, sys.exit(0), None) Parce que la fonction évalue ses arguments avant l'appel, alors que la structure if n'évalue que le corps de la branche pertinente, après avoir terminé le test.Pouvons-nous reproduire ce comportement en fonctionnel ? Oui, on peut, en retardant l'évaluation au moyen de fonctions constantes: Code PYTHON :
def my_if(test,good,bad): Ici, les lambdas définissent des fonctions sans argument. On peut les passer a d'autres fonctions, sans que leur corps ne soit évalué; on déclenche l'évaluation avec (). Itérateurs L'avantage du style fonctionnel, c'est qu'il se compose facilement. Par exemple, il est plus facile d'empiler des maps/filters que des boucles for: Code PYTHON :
def even(x): Ici, il est très facile de séparér les deux opérations (filtrer les éléments pairs, et leur appliquer une transformation affine.) La version basée sur les boucles mélange les deux opérations, ce qui rend le code moins modulaire et moins clair: Code PYTHON :
out = [] Malheureusement, avec les implémentations de map et filter telles qu'elles ont été présentées dans la Partie I, la version fonctionnelle se traduit en: Code PYTHON :
out1 = [] Ce qui est beaucoup moins efficace, puisqu'on alloue une liste intermédiaire pour la consommer et la détruire aussitôt. Nous allons résoudre ce problème, en utilisant le truc de l'évaluation paresseuse pour représenter une liste dont les éléments sont évalués un à un. Code PYTHON :
class StopIteration(Exception): Cette classe, a partir d'une liste classique, construit une liste paresseuse qui renvoie ses éléments un par un, à chaque appel de la méthode next(). Nous pouvons maintenant définir notre fonction map sur les LazyLists, sous forme d'une classe: Code PYTHON :
class Map(object): Nous pouvons aussi définir Filter: Code PYTHON :
class Filter(object): Notez l'utilisation astucieuse des exceptions, qui permet de propager naturellement les fins de liste le long de la chaine sans avoir a écrire explicitement du code pour le gérer. Manque encore une méthode dans LazyList pour la convertir en liste normale: Code PYTHON :
def toList(self): Je peux maintenant écrire Code PYTHON :
out = Map(lambda x: 3*x + 1, Filter(lambda x: x % 2 == 0, input)).toList() Et le code aura le même comportement désirable que la boucle for, qui consiste a traiter les éléments un par un le long de tout le pipeline. Dans la librairie standard, Iterator s'appelle en fait iter. A noter que la boucle for accepte implicitement un itérateur, on peut donc écrire: for x in Map(lambda x: ...., src): .... Coroutines Le concept d'itérateur devient particulièrement puissant si on le combine avec celui des coroutines. Les coroutines sont une primitive de programmation concurrente, encore plus primitive que les threads. Comme les threads, les coroutines s'exécutent chacune avec leur propre stack, mais pas en parallèle; le passage d'une coroutine a une autre se fait coopérativement, en des points-clef du code, plutôt que par préemption. Les coroutines s'écrivent en python à l'aide du mot-clef yield. Une fonction qui contient un mot yield n'est plus une fonction, mais un objet itérateur. yield fonctionne un peu comme un return, mais sauvegarde l'état de la fonction, de sorte que l'exécution reprend à la suite du yield au prochain appel à next(). Voici un exemple simple (bien que peu utile) pour en visualiser le flot: Code PYTHON :
def my_coroutine(): Voyons pas à pas ce qui se passe dans my_main:
Au final, on aura eu: x=1, a=x+2, y=a2, b=y+3, z=b3. L'exécution de la coroutine et de la fonction principale sont entrelacées. De manière générale, les coroutines de ce style, bidirectionnelles, sont dangereuses. Les débugger peut être un vrai cauchemar. Beaucoup moins dangereuses, sont les coroutines qui ne communiquent que dans une seule direction. Deux scénarios possibles:
On peut donc facilement réécrire nos combinateurs précédents en termes de générateurs: Code PYTHON :
def map(f, src): Générateurs Les générateurs peuvent faire mieux que ça. Par exemple, vous savez peut-être que les fichiers ouverts en mode texte implémentent une interface d'itérateur qui permet de traiter les lignes une a une: Code PYTHON :
for line in open('fichier'): Et si nous voulions lire, non pas des lignes, mais des mots ? Code PYTHON :
def words(lines): On peut ensuite faire: Code PYTHON :
for word in words(open('fichier')) Et, contrairement à une fonction classique, cet usage de words() ne cause évidemment pas le chargement du fichier entier en mémoire; la boucle de découpage de words tournera entrelacée avec celle du programme principal. Notez que dans les versions récentes, il existe une notation raccourcie équivalente: Code PYTHON :
def words(lines): Voici un exemple de générateur qui ne termine pas, et qui génère une infinité de nombrs entiers. Code PYTHON :
def enum(): Voici un générateur qui garde les n premiers éléments d'un itérateur: Code PYTHON :
def head(n, src): Est équivalent à notre Code PYTHON :
out = Map(lambda x: x*3, Filter(lambda x: x%2 == 0, src)) Et offre le comportement d'entrelacement que l'on souhaite. |
|
Sujets apparemment similaires… | |||||
Sujet | Auteur | Réponses | Affichages | Dernier message | |
La Prog Fonctionnelle, Partie I | b0fh | 1 | 1,320 |
08-04-2016, 22h40 Dernier message: lanodan |
|
La Prog Fonctionnelle, Partie II | b0fh | 0 | 802 |
20-09-2014, 12h44 Dernier message: b0fh |
|
La Prog Fonctionnelle, Partie I bis | b0fh | 0 | 804 |
19-09-2014, 11h09 Dernier message: b0fh |
Utilisateur(s) parcourant ce sujet : 1 visiteur(s)