Programmation Fonctionnelle Avancée 4 : Évaluation paresseuse

publicité
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
Téléchargement