Programmation Fonctionnelle I, 2009-2010 L1 Info&Math - Faculté des Sciences de Nice Cours n°11 http://deptinfo.unice.fr/~roy Les Arbres Binaires d'Expressions (2) Parcours Itératif Calcul Formel Le type abstrait PILE empiler dépiler • Les piles forment un type de donnée fonctionnant en ordre LIFO [Last In, First Out]. sommet • Nos piles seront fonctionnelles. La fonction (empiler x P) ne modifie pas la pile P, elle se contente de calculer une nouvelle pile ! Toutes les opérations sont en O(1). > > 3 > 4 > 3 (pile-vide) (pile-vide? P) ; constructeur de pile vide ; Pile → Boolean (empiler x P) ; Elément × Pile → Pile* (depiler P) ; Pile* → Pile (sommet P) ; Pile* → Elément où Pile* dénote l'ensemble des piles non vides. (define P (empiler 3 (empiler 4 (empiler 5 (pile-vide))))) (sommet P) (sommet (depiler P)) (sommet P) 3 4 5 • L'implémentation du type abstrait passe ici aussi par les listes, qui sont bien pratiques ! (define (pile-vide) empty) (define (pile-vide? pile) (empty? pile)) Pile → Boolean ∅ → Boolean (define (empiler x pile) (cons x pile)) Elément × Pile → Pile* (define (sommet pile) (if (empty? pile) (error 'sommet "Pile vide !") (first pile))) Pile* → Elément (define (depiler pile) (if (empty? pile) (error 'depiler "Pile vide !") (rest pile))) Pile* → Pile N.B. Les fichiers adt-arbre.scm et adt-pile.scm sont rédigés sous la forme de modules [horsprogramme], avec une syntaxe un peu particulière... A quoi sert une PILE ? • En électricité, à fournir de l'énergie. En cuisine, à stoker des assiettes. En programmation, à stocker des résultats intermédiaires qu'il faudra utiliser plus tard ! • Par exemple, pour exécuter une récurrence enveloppée, une pile implicite [non visible au programmeur] est utilisée par le système. (fac n) = (* (fac (- n 1)) n) Pour calculer n! , je calcule (n-1)! mais auparavant je mets la multiplication par n en attente dans une pile... calculer 3! calculer 2! calculer 1! calculer 0! * 1 * 3 6 2 * 2 * 2 * 3 * 3 1 1 • Les ordinateurs sont des machines à pile. Lorsqu'un logiciel fonctionne sur votre machine, une pile invisible et silencieuse est utilisée... • Le programmeur lui-même peut avoir besoin de modéliser une pile dans le programme qu'il rédige. Reprenons la calculette HP [cours 10 p. 14] > (scheme->hp '(- (* 2 (+ 5 1)) (/ 4 2))) (2 enter 5 enter 1 enter + * 4 enter 2 enter / -) • Nous savons traduire une expression Scheme [arbre] en une séquence de touches pour HP. Maintenant nous aimerions jouer itérativement cette séquence de touches ! Pour cela, il nous faut une pile explicite : 2 enter 5 enter 2 1 enter + * 5 5 6 2 2 2 / 10 1 2 2 4 12 12 2 enter 12 4 enter 4 12 (define (exec-HP codeHP) ; on suppose codeHP syntaxiquement correct ! (local [(define (iter L pile) (begin (printf "L=~a pile=~a\n" L pile) ; pour le debug... (cond ((empty? L) (sommet pile)) ((number? (first L)) (iter (rest (rest L)) (empiler (first L) pile))) (else (local [....] (iter (rest L) ...))))))] cf TP! (iter codeHP (pile-vide)))) • Exemple : > (scheme->hp '(- (* 2 (+ 5 1)) (/ 4 2))) (2 enter 5 enter 1 enter + * 4 enter 2 enter / -) > (exec-HP '(2 enter 5 enter 1 enter + * 4 enter 2 enter / -)) L=(2 enter 5 enter 1 enter + * 4 enter 2 enter / -) pile=() L=(5 enter 1 enter + * 4 enter 2 enter / -) pile=(2) L=(1 enter + * 4 enter 2 enter / -) pile=(5 2) L=(+ * 4 enter 2 enter / -) pile=(1 5 2) L=(* 4 enter 2 enter / -) pile=(6 2) L=(4 enter 2 enter / -) pile=(12) L=(2 enter / -) pile=(4 12) L=(/ -) pile=(2 4 12) L=(-) pile=(2 12) L=() pile=(10) 10 Une application des piles : parcours itératif d'un arbre • Les arbres binaires formant un type de donnée récursif, il est naturel de les programmer récursivement ! • Mais il peut être intéressant de produire un algorithme itératif. • La présence de deux fils introduit des mises en attente. Munissonsnous donc d'une pile en paramètre et empilons-y les sous-arbres qu'il restera à visiter ! • Exemple : soit à calculer le nombre de feuilles d'un arbre A. STRATEGIE DU PARCOURS EN PROFONDEUR ITERATIF : - si je suis sur un noeud, j'empile [un pointeur sur] le fils droit et je vais visiter le fils gauche. - si je suis sur une feuille, je regarde s'il reste un arbre à visiter au sommet de la pile. (define (nb-feuilles A) ; le nombre de feuilles arbre, pile courante, résultat courants (local [(define (iter A P acc) ; (begin (printf "A=~a P=~a acc=~a\n" A P acc) ; pour le debug... (if (feuille? A) (if (pile-vide? P) (+ acc 1) (iter (sommet P) (depiler P) (+ acc 1))) (iter (fg A) (empiler (fd A) P) acc))))] (iter A (pile-vide) 0))) > (nb-feuilles '(- (* 2 (+ x 1)) (/ x y))) A=(- (* 2 (+ x 1)) (/ x y)) P=() acc=0 A=(* 2 (+ x 1)) P=((/ x y)) acc=0 - A=2 P=((+ x 1) (/ x y)) acc=0 A=(+ x 1) P=((/ x y)) acc=1 / * A=x P=(1 (/ x y)) acc=1 A=1 P=((/ x y)) acc=2 A=(/ x y) P=() acc=3 A=x P=(y) acc=3 2 x + A=y P=() acc=4 ==> 5 x 1 y • Exemple d'analyse syntaxique itérative : reconstruction d'un arbre à partir de son parcours postfixe [cf Cours 10 pp. 17-20]. On analyse itérativement le parcours postfixe : (2 3 5 / 8 * +) + * cf TP ! / 2 et on reconstruit l'arbre d'origine : • Vous visualisez la pile ?... 3 5 8 (+ 2 (* (/ 3 5) 8)) Introduction au Calcul Formel Maxima Qu'est-ce que le CALCUL FORMEL ? • Le calcul formel [computer algebra] consiste à pratiquer une algorithmique symbolique sur des objets mathématiques. • Symbolique est apposée à numérique : on travaille sur des formes exactes plutôt que sur des approximations numériques. f'(2) → 1.6756288108066286 f(x) = 2 cos(3x) calcul numérique f'(x) → -6 sin(3x) calcul symbolique ou formel • Les premiers gros systèmes de calcul formel furent REDUCE [1968] et MACSYMA [1970], tous deux écrits en LISP. MACSYMA existe encore, on peut le télécharger [http://maxima.sourceforge.net]. http://fr.wikipedia.org/wiki/Logiciel_de_calcul_formel (logiciel libre !) Le Problème de la Simplification • C'est en fait le problème central ! Comment réduire les expressions à des formes irréductibles, de préférence canoniques [pour l'unicité] ? • Un évaluateur peut calculer la valeur numérique d'un arbre : (valeur '(+ (* 2 3) 5)) → 11 (valeur '(+ (* x 2) y)) '((x 3) (y -1))) → 5 • Un simplificateur sait travailler avec des arbres algébriques, en présence d'inconnues : (simplif '(+ (* x (- 5 4)) (+ 1 (* 0 y)))) → (+ x 1) • Il s'agit donc bien d'un algorithme formel, avec localement quelques calculs sur constantes, comme celui de (- 5 4). Algebraic simplification: a guide for the perplexed Joel Moses, Project MAC, MIT, 1971 • Nous essayons de simplifier nos arbres binaires d'expressions. Pour être modulaire, le toplevel du simplificateur se contente d'aiguiller sur des simplificateurs spécialisés : (define (simplif A) (if (feuille? A) A (case (racine A) ((+) (simplif+ A)) ((-) (simplif- A)) ((*) (simplif* A)) ((/) (simplif/ A)) (else (error 'simplif (format "Opérateur inconnu ~a" (racine A))))))) • Chaque spécialiste va implémenter ses propres règles de réduction : (define (simplif+ A) ; on sait que A est un arbre de racine + (local [(define Ag (simplif (fg A))) (define Ad (simplif (fd A)))] (cond ((and (number? Ag) (number? Ad)) (+ Ag Ad)) ((equal? 0 Ag) Ad) ((equal? 0 Ad) Ag) (else (arbre '+ Ag Ad))))) etc, cf TP ! Le Problème de la Dérivation Symbolique A = y/x → ∂ ∂x A = -y/x2 ∂ ∂y A = 1/x ∂ ∂z A = 0 • Il s'agit de dériver un arbre d'expression A par-rapport à une variable v. Ici aussi le toplevel du dérivateur va aiguiller sur des dérivateurs spécialisés. Le résultat doit être simplifié autant que faire se peut car la taille des arbres dérivés croît très vite ! (define (diff A v) ; retourne ∂A/∂v (if (feuille? A) (if (equal? A v) 1 0) ; ∂v/∂v=1 et ∂u/∂v=0 (simplif (case (racine A) ((+ -) (diff+- A v)) ((*) (diff* A v)) ((/) (diff/ A v)) (else (error 'diff (format "Op. inconnu" (racine A))))))) • On se laisse guider par les règles de dérivation usuelles : ∓ A B ' A ∓ A' B' * B ' + * A' B A / B ' * A B' / * * * B etc B A' B A B' • Voici par exemple le spécialiste de la dérivation d'une somme : (define (diff+- A v) ; A = ( Ag Ad) (arbre (racine A) (diff (fg A) v) (diff (fd A) v))) ∓ • Notez la récurrence croisée : diff utilise diff+- et inversement ! Le Problème de l'Intégration Symbolique • On parle bien de recherche d'une primitive, pas de calcul numérique ! • Hélas le problème est infiniment plus difficile ! Si la dérivation est purement mécanique, l'intégration requiert de l'expertise... • Stratégie ? Intégration par parties, changement de variables ? • PROBLEME : certaines fonctions n'ont PAS de primitive exprimable comme combinaison de fonctions élémentaires. (algorithme de Risch, 1969) • Les logiciels de calcul formel font de leur mieux... ! x2 e dx Arbres et Fonctions • Un arbre est une expression, pas une fonction ! Comme si l'on 2 2 confondait x + 1 et x !→ x + 1 • Comment construire la fonction associée à un arbre ? - on part d'un arbre A, par exemple (* x (+ x 2)), en la variable x. - on sait prendre sa valeur en un point x0 avec (valeur A AL) - on sait construire une fonction avec le constructeur lambda. (define (arbre->fonction A v) ; v est la variable (lambda (x0) (valeur A (list (list v x0))))) x, y, ... > (define A '(* x (+ x 2))) > (define f (arbre->fonction A 'x)) > (map (lambda (x) (list x (f x))) (list 0 1 2 3 4)) ((0 0) (1 3) (2 8) (3 15) (4 24)) N.B. On a passé la variable de l'arbre en paramètre. On pourrait aussi la trouver automatiquement (s'il n'y en a qu'une)... Dessiner la courbe d'une fonction • Bon, la fonction fonctionne. Si l'on veut en plus dessiner sa courbe, on demande le module plot : (require (lib "plot.ss" "plot")) > (define df (arbre->fonction (diff A 'x) 'x)) > (plot (mix (line f (color 'blue) (width 2)) (line df (color 'red) (width 2))) (x-min -4) (x-max 3) (y-min -3) (y-max 8)) ; la dérivée Dem sur ande le m z de ot p l'ai lot. de ..