Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse
Transcription
Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse
Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Plan du cours Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Ralf Treinen I Stratégies d'évaluation I Évaluation stricte et évaluation paresseuse Université Paris Diderot I Implémenter la paresse en OCaml UFR Informatique I Le module Laboratoire Preuves, Programmes et Systèmes Lazy de OCaml [email protected] 27 octobre 2016 c Roberto Di Cosmo et Ralf Treinen Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation L'ordre d'évaluation I Quand on programme, on a souvent envie de, ou besoin de, savoir dans quel ordre nos commandes ou expressions sont évaluées. I Il n'est pas toujours simple de répondre, avec les langages actuels. I Par exemple : I I Ordre d'évaluation des arguments dans l'appel d'une fonction (procédure, méthode) ? Ordre d'évaluation des sous-expressions combinées par un opérateur ? Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation Exemple : C #include <s t d i o . h> int f o o ( int x , int { return x+y ; y) } int main ( ) int i = 1 ; { /∗ order of evaluation of arguments ∗/ /∗ order of evaluation of summands ∗/ p r i n t f ( "%d\n" , f o o ( i ++, i − − )); } p r i n t f ( "%d\n" , f o o ( i ++,1)+ f o o ( i − − ,1)); Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation Stratégies d'évaluation Sur l'exemple en C La stratégie d'évaluation dans les langages fonctionnels Pour les langages fonctionnels, une question majeure est de savoir I On obtient des résultats diérents avec les compilateurs gcc ce qu'il se passe quand on applique une fonction : (ici version 5) et clang (ici version 3.5). 1. Est-ce que le compilateur essaye d'évaluer le corps d'une fonction avant qu'elle soit appliquée ? I C Standard, subclause 6.5 [ISO/IEC 9899 :2011] : Except as specied later, side eects and value computations of subexpressions are unsequenced. Si on écrit fun x -> x+fact(100), est-ce que le factoriel est calculé à chaque appel ou une seul fois ? (e a) (e appliquée à a) est-ce e ou a ? (fun x -> e) a, est-ce qu'on évalue l'argument a 2. Si on écrit une expression I C.-à-d. : L'ordre d'évaluation des arguments dans un appel de qu'on commence par évaluer fonction n'est pas spécié. I C'est pour traiter ces questions qu'on étudie la 3. Si on écrit sémantique des langages de programmation ! d'abord, ou fait-on le passage de paramètres d'abord ? On ne peut pas trouver la réponse à ces questions par des tests car le comportement peut être non spécié ! Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation Stratégies d'évaluation Question 1 : Pas d'évaluation sous le λ Exemples (lambda.ml) I OCaml n'essaye pas d'évaluer le corps d'une fonction avant qu'elle ne soit appliquée à un argument. let I Tous les langages fonctionnels font ce choix. I Il y a plein de bonnes raisons d'arrêter l'évaluation quand on rencontre une abstraction (aussi appelée lambda du utilisé dans le cours de λ-calcul Sémantique pour traiter ces questions de façon générale). I Pas confondre avec des optimisation de code faites par le compilateur, comme propagation de constantes. f = function x ( ∗ pas d ' e r r e u r ∗ ) f 42;; −> 1/0 ∗ x ;; (∗ e x c e p t i o n d i v i s i o n par z e r o ∗) Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation Stratégies d'évaluation Question 2 : non spécié Exemples (order.ml) ( ∗ l ' o r d r e n ' e s t pas s p e c i f i e ∗ ) I OCaml manual, section 6.7 (Expressions) : The expression expr arg . . . argn . . .. The order in which the expressions expr, arg , . . ., argn are evaluated is not specied. 1 1 I Si on a vraiment besoin d'un ordre spécique il faut le forcer avec la construction let ... in ... Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation ( print_string " gauche \n" ; fun ( print_string " d r o i t e \n" ; 42) x −> x) ;; ( ∗ f o r c e r un o r d r e d ' e v a l u a t i o n ∗ ) l e t f = p r i n t _ s t r i n g " g a u c h e \ n " ; fun i n f ( p r i n t _ s t r i n g " d r o i t e \n" ; 42) x −> x Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation Question 3 : On évalue l'argument avant de le passer en Exemples (strict.ml) paramètre I Le choix de OCaml est d'évaluer l'expression fonctionnelle et les arguments d'abord. ( ∗ l ' argument d ' abord . . . ∗ ) let évalué, en paramètre à la fonction, et ne lancent le calcul qu'au moment où on aura besoin de son résultat. I Cela permet à Haskell, par exemple, de ne jamais calculer fact 100 dans une expression (fun x -> 3) (fact 100) x −> print_string ( print_string I Ce choix n'est pas le seul possible : d'autres langages fonctionnels, comme Haskell, passent d'abord l'argument, non r = ( fun " c o r p s \n" ; x + x) " argument \n" ; 3 5 + 2 4 ) ; ; ( ∗ l ' argument e s t t o u j o u r s e v a l u e ∗ ) let r = ( fun x −> print_string ( print_string " c o r p s \n" ; 0) " argument \n" ; 3 5 + 2 4 ) ; ; Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Stratégies d'évaluation OCaml fait de l'évaluation stricte Évaluation stricte ou paresseuse ? I Avantages des l'évaluation stricte : stratégie d'évaluation. Celle utilisée par OCaml est appelée évaluation stricte I L'ensemble de ces choix s'appelle une I I Toutes les fonctions f qu'on peut écrire en OCaml sont strictes : f (⊥) = ⊥, où ⊥ est un calcul inni. I (voir plus en profondeur dans le cours de Sémantique). I I plus facile à mettre en ÷uvre. la complexité est plus prévisible. I Avantages de l'évaluation paresseuse : I I un argument pas utilisé n'est pas évalué. permet de travailler avec des structures innies ! I Inconvénient de l'évaluation paresseuse : il nous faut un mécanisme pour mémoriser un argument évalué (pour éviter qu'il soit évalué plusieurs fois). Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Évaluation paresseuse dans un langage strict Structures innies Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Évaluation paresseuse dans un langage strict Évaluation paresseuse I Exemple : Pour écrire un algorithme combinatoire, on a besoin de calculer souvent le factoriel d'un entier naturel. I Nous ne voulons pas le recalculer à chaque fois qu'on en a besoin ; on préfère garder la liste de tous les factoriels. I Pour cela, on écrit le code suivant, mais on s'aperçoit qu'il ne fait pas ce que l'on veut (pourquoi ?) : let rec f a c t = fun 0 −> 1 | n −> n ∗ ( f a c t ( n − 1 ) ) ; ; let rec f a c t _ f r o m n = ( f a c t n ) : : ( f a c t _ f r o m ( n + 1 ) ) ; ; let f a c t _ n a t = f a c t _ f r o m 0 ; ; Notre dé : I on veut que la liste (innie) ne soit pas calculée tout de suite I mais seulement au fur et à mesure, quand on a besoin d'en prendre des éléments Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Évaluation paresseuse dans un langage strict Évaluation paresseuse dans un langage strict Évaluation paresseuse du pauvre, avec les fermetures I En protant du fait qu'en OCaml on n'évalue pas le corps d'une fonction, il est possible de simuler un calcul paresseux en protégeant le calcul par une fonction. I On change la dénition du type des listes pour prévoir une queue de liste dont l'évaluation est bloquée sous une abstraction. I Une liste innie paresseuse a la forme fun () -> Cons(e1 , fun () -> Cons(e2 , . . .)) où ei est l'expression (non évaluée car protégée par l'abstraction) qui donne le i -ème élément de la liste. let f a c t n = let rec c a l c = function 0 −> 1 | n −> n ∗ ( c a l c in P r i n t f . p r i n t f " c a l c u l e f a c t (%d )\ n" n ; c a l c type ' a l l t i p = N i l | LCons of ' a ∗ ' a l a z y l i s t and ' a l a z y l i s t = u n i t −> ' a l l t i p let rec f a c t _ f r o m n = fun ( ) −> LCons ( f a c t n , f a c t _ f r o m ( n +1)) let rec t a k e n s = match n with | 0 −> [ ] | n −> match s ( ) with N i l −> [ ] ( n − 1)) n | LCons ( v , r ) −> v : : ( t a k e ( n − 1) r ) ; ; let fact_nat = fact_from 0 ; ; take 5 fact_nat ; ; Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Évaluation paresseuse dans un langage strict Évaluation paresseuse dans un langage strict Limites de notre évaluation paresseuse du pauvre I La petite astuce avec les fermetures permet d'écrire des La bonne solution : Paresse + partage ! I Chaque fois qu'une partie d'une structure de données dégelée et calculée, on veut qu'elle soit structures de données paresseuses, mais ce n'est pas encore ce paresseuse est que nous voulons ! remplacée, silencieusement, par le résultat de ce calcul dans la I Notre code permet de décrire des listes innies, mais chaque fois qu'on visite la même liste, on relance le calcul de ses éléments. C'est très inecace ! structure de donnée. I La prochaine fois qu'on y accède, on veut retrouver la partie de la liste innie déjà évaluée. Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Évaluation paresseuse dans un langage strict Évaluation paresseuse dans un langage strict Exemples (lazy3.ml) let rec t a k e _ l ( n : i n t ) ( s : ' a c l e v e r l i s t ) = let rec take_and_update n ( l l : ' a c l e v e r l i s t _ c e l l ) ∗ ∗ i f n=0 then [ ] else match l l ( ) with LCons ( v , r ) −> let s r = ref ( L r ) in ( type ' a l l t i p = L C o n s of and ' a c l e v e r l i s t _ c e l l = | Nil | Cons | L and of 'a of 'a ( unit 'a ∗ ( unit −> 'a lltip ) ∗ 'a c l e v e r l i s t −> ' a l l t i p ) cleverlist = 'a cleverlist_cell l e t fact_nat = l e t re c a u x n = ( fun ( ) −> LCons ( f a c t i n r e f ( L ( aux 0 ) ) n, aux debut , ! s = L( l l ) ) s := ( Cons ( v , s r ) ) ; v : : ( take_and_update ( n − 1) r s r ) ref in i f n=0 then [ ] else match ! s with ( n +1))) ;; ;; au | N i l −> [ ] | Cons ( v , r ) −> v : : ( t a k e _ l ( n − 1) r ) | L ( l l ) −> take_and_update n l l s take_l 5 fact_nat ; ; Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Évaluation paresseuse dans un langage strict Une solution ecace et Le module fonctionnelle ? I C'était un exercice un peu pénible ... donne l'expression e en forme paresseuse ? I Non, lazy ne peut pas être une fonction, car OCaml évalue toujours l'argument avant d'appliquer une fonction. I Il nous faut un support par le système OCaml : le module Lazy, et un mot-clé spécique. en OCaml Le module I Peut-on simplement écrire un module qui fournit une fonction lazy, telle que (lazy e) Lazy Lazy de la librairie OCaml type ' a t exception U n d e f i n e d v a l f o r c e : ' a t −> ' a v a l l a z y _ i s _ v a l : ' a t −> I une valeur de type 'a Lazy.t bool est appelée une contient un calcul paresseux de type 'a. suspension et I on construit des valeurs de type ' a Lazy. t avec le mot clé réservé lazy : l'expression contenant le calcul expr lazy (expr) crée une suspension sans de l'évaluer. I Lazy. force s force l'évaluation de la suspension s, renvoie le résultat, et remplace la valeur dans la structure de donnée : appelé sur une partie déjà dégelée, il ne refait pas le calcul. I Lazy.lazy_is_val s teste si la suspension s est déjà dégelée. si s = Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Le module Lazy Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse en OCaml Le module Exemples (stream1.ml) type and streamtip = Nil 'a stream = streamtip 'a l e t re c f a c t _ f r o m l a z y ( Cons ( f a c t | 0,s | n, s let take n | 5 'a ∗ 'a fact_from (n+1)));; (s : 'a stream ) = match ( Lazy . f o r c e −> Nil | Cons ( v , r ) stream s) (n , s ) Lazy l'argument (). exactement là où on avait forcé l'évaluation en appliquant à with I La promesse est tenue : le calcul des premiers 5 éléments est with fait [] seulement la première fois. I On va modier le code de telle sorte que le calcul de chaque −> v :: ( take ( n − 1) r );; nouvel élément de fact_nat utilise une seule multiplication. 0;; fact_nat ; ; Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Le module fermetures lazy exactement là où on avait mis des fun () ->, et on a placé des Lazy.force I On a placé les n, | Lazy Lazy . t ; ; = fact_nat = fact_from take of Cons n −> [ ] −> match en OCaml Streams : la liste des factoriels ré-visitée avec 'a l e t re c Lazy Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse en OCaml Le module Exemples (stream2.ml) Lazy en OCaml Syntaxe plus moderne En OCaml il est possible d'utiliser le mot clé lazy même dans les motifs utilisés dans les dénitions par cas ; cela permet souvent de l e t re c f a c t _ f r o m _ f a s t lazy ( let next = n ∗ Cons ( n e x t , n prev prev : int stream = in fact_from_fast let fact_nat_fast = l a z y ( Cons ( 1 , f a c t _ f r o m _ f a s t se passer de l'usage de Lazy.force. Par exemple, un morceau de code tel que 1 ( n + 1) 1)) ;; next ) ) ; ; match Lazy . f o r c e −> | Nil | Cons s with ... (_, tl ) −> ... peut s'écrire de façon équivalente comme : take 50 fact_nat_fast ; ; match s with | l a z y N i l −> . . . | l a z y ( C o n s (_, t l )) −> ... Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Le module Lazy Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse en OCaml Le module Attention à l'évaluation stricte ! Lazy en OCaml Les opérations de base sur les streams ou ots Est-ce que ces deux fonctions ont le même comportement ? l e t re c t i m e s 1 n = f u n c t i o n | l a z y N i l −> l a z y N i l | l a z y ( C o n s ( h , t ) ) −> l a z y ( Cons ( n∗h , t i m e s 1 let times2 n s = l e t re c t i m e s _ a u x = f u n c t i o n | l a z y N i l −> N i l | l a z y ( C o n s ( h , t ) ) −> ( C o n s ( n ∗ h , l a z y i n l a z y ( times_aux s ) ; ; let _ = times1 42 ( fact_from 1);; let _ = times2 42 ( fact_from 1);; n t )) Lazy I Ne pas confondre avec le module ( times_aux 'a stream : int −> 'a −> 'a stream −> 'a stream stream −> 'a stream ( ∗ l e stream s a n s l e s p r e m i e r s n e l e m e n t s ∗ ) val val end ; ; drop : reverse int : −> 'a 'a Lazy en OCaml module ( ∗ un stream c o n t e n a n t l e s p r e m i e r s n e l e m e n t s ∗ ) take analyseurs récursifs descendants. stream ( ∗ c o n c a t e n a t i o n de s t r e a m s ∗ ) 'a qui existe déjà dans Un module pour les streams II module type STREAM = s i g type ' a s t r e a m h e a d = N i l | C o n s of ' a ∗ and ' a s t r e a m = ' a s t r e a m h e a d L a z y . t val t ))) Le module : Stream la librairie standard OCaml et qui sert à construire des Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse en OCaml (++) écrire un module pour streams. Un module pour les streams I val Lazy, les ots (séquences potentiellement innies). En Anglais : Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Le module I On peut maintenant, à l'aide de stream stream −> −> 'a 'a stream stream Stream : STREAM = type ' a s t r e a m h e a d and ' a s t r e a m = ' a = Nil struct | Cons streamhead of 'a ∗ 'a stream Lazy . t (∗ c o n c a t e n a t i o n ∗) l e t (++) s 1 s 2 = l e t re c a u x s = match s with | l a z y N i l −> L a z y . f o r c e s 2 | l a z y ( C o n s ( hd , t l ) ) −> C o n s i n l a z y ( aux s1 ) ( hd , lazy ( aux t l )) Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Le module Lazy Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse en OCaml Le module Un module pour les streams III | 0, _ | n, s n −> N i l −> match | N i l −> | Cons in lazy ( take ' n Lazy . f o r c e n, s s tl ) match with ( take ' (n − 1) t l )) ( ∗ l e stream a p r e s n e l e m e n t s , m o n o l i t h i q u e ! ∗ ) Lazy . f o r c e s Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse Le module Lazy −> match s with l a z y N i l −> N i l | l a z y ( C o n s (_, t l ) ) −> ( d r o p ' with 0 −> s | n −> l a z y ( d r o p ' n n (n − 1) tl ) s) ( ∗ r e t o u r n e un stream , m o n o l i t h i q u e ! ∗ ) s) l e t drop n s = l e t re c d r o p ' n s = match n with 0 −> n with −> lazy ( hd , | | Nil ( hd , Cons match s = en OCaml Un module pour les streams IV ( ∗ c o p i e p a r e s s e u s e des p r e m i e r s n e l e m e n t s ∗ ) let take n s = l e t re c t a k e ' Lazy let reverse s = l e t re c r e v e r s e ' a c c s = match s with | l a z y N i l −> a c c | l a z y ( C o n s ( hd , t l ) ) −> r e v e r s e ' ( C o n s ( hd , l a z y a c c ) ) lazy ( reverse ' Nil s ) end ; ; tl in Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse en OCaml Le module Remarques Lazy en OCaml Exercices Réalisez les fonctions supplémentaires suivantes : Les opérations drop et reverse sont monolithiques, dans le sens que, dès qu'on essaye de prendre le premier élément du stream résultant : I pour I pour drop n s, les premiers n éléments de s reverse s, tout le stream s est forcé. sont forcés On ne suspend donc que le début de l'opération, mais une fois qu'on essaye d'en voir le résultat, toute l'opération est exécutée d'un coup. module type STREAMEXT = s i g i n c l u d e module type of S t r e a m v a l o f _ l i s t : ' a l i s t −> ' a s t r e a m v a l t o _ l i s t : ' a s t r e a m −> ' a l i s t v a l p u s h : ' a −> ' a s t r e a m −> ' a s t r e a m v a l p o p : ' a s t r e a m −> ( ' a ∗ ' a s t r e a m ) o p t i o n v a l map : ( ' a −> ' b ) −> ' a s t r e a m −> ' b s t r e a m v a l f i l t e r : ( ' a −> b o o l ) −> ' a s t r e a m −> ' a s t r e a m v a l s p l i t : ' a s t r e a m −> ' a s t r e a m ∗ ' a s t r e a m v a l j o i n : ' a s t r e a m ∗ ' a s t r e a m −> ' a s t r e a m end ; ; Assurez vous que votre réalisation est bien paresseuse. in