Module programmation: évaluation.





Plan des 4 prochains cours


Renaud Rioboo1

1
Renaud.Rioboo@lip6.fr



Évaluation


Espace d'expressions Espace de valeurs
1+2 3
if true then 1 else 2 1
let y = 1 in y+1 2
function x -> x + 1 «x:->x+1,E»
let y = 1 in
  function x -> x + y
«x:->x+y,E1»

Une expression Caml-Light prend une valeur dans un environnement. C'est l'évaluation.

Un identificateur est un nom2.

Un environnement permet d'associer une valeur à un identificateur.

Une liaison est l'association d'un identificateur à une valeur.

Dans un environnement il peut y avoir plusieurs liaisons pour un même identificateur, c'est le masquage.

Si E est un environnement, E est une expression et V une valeur, on notera E -E-> V si E s'évalue en V dans l'environnement E.

Ainsi si E est l'environnement courant, on a : L'environnement global est l'environnement dans lequel le programme se trouve.


2
que l'on utilise souvent dans une fonction ou pour pour éviter des re-calculs



Portée dynamique


La portée dynamique n'est pas compatible avec le typage statique.


Portée statique





Interaction avec le système de fichiers


Eg = Eu Å Em Å Eb

L'environnement global est la juxtaposition de Une compilation (camlc -c) produit : La directive #open mod permet de rendre visible les identificateurs décrits dans mod.zi sans avoir à les nommer par mod__ident.

L'interprète possède En mode compilé il faut inclure le fichier .zo dans la ligne de commande

camlc -o bar foo.zo bar.ml

pour obtenir les valeurs définies dans le module foo et rendre visibles les identificateurs de foo en incluant la directive
#open "foo"
dans le fichier bar.ml


Terminologie


MATHS INFORMATIQUE
variable paramètre formel
paramètre paramètre libre
argument paramètre effectif
fonction fonction partiellement définie
application fonction
calculer f(exp) appliquer f à exp
f(exp) f exp
On doit distinguer l'expression f(exp) de la valeur qu'elle prend dans un environnement. On appelle application une expression de la forme E1 E2.


Les fonctions


Les fonctions sont des valeurs Caml-Light que l'on nomme des fermetures. En Caml-Light elles sont caractérisées par : On les note : «nom:->corps,Env»

On notera souvent


Appel par Nom


let deux = (print_string "1+1" ; 1 + 1) in
  deux + deux ;;
  1. liaison de deux à
    (print_string "1+1" ; 1 + 1)
    puis évaluation de deux + deux
  2. substitution de deux à sa ``valeur'' soit :
    (print_string "1+1" ; 1 + 1) +
    (print_string "1+1" ; 1 + 1)
    
  3. affichage de "1+1" calcul de 1 + 1 = 2
  4. affichage de "1+1" calcul de 1 + 1 = 2
  5. calcul de 2 + 2 = 4
Les identificateurs sont remplacés par des expressions qui sont réévaluées.




Appel par Valeur


let deux = (print_string "1+1" ; 1 + 1) in
  deux + deux ;;
  1. calcul de la valeur de
    (print_string "1+1" ; 1 + 1)
  2. affichage de "1+1" retour de 2
  3. liaison de deux à sa valeur 2
  4. calcul de deux + deux soit 2+2
  5. retour de 4
Les identificateurs sont remplacés par des valeurs et ne sont pas réévalués.




La liaison





Les fonctions comme valeurs


On peut étendre les opérations booléennes aux fonctions d'une variable booléenne :
type fonctions_booleennes = Fbool of bool -> bool ;;

let vrai = Fbool (function x -> true)
and faux = Fbool (function x -> false);;

let ou = function f1 -> function f2 ->
  match (f1,f2) with
   ((Fbool f1), (Fbool f2)) -> 
      Fbool (function x -> (f1 x) || (f2 x)) ;;




Les fonctions sont des valeurs spéciales


let applique = function f1 -> 
  match f1 with 
    Fbool f1 -> function x -> f1 x ;;

let est_vrai = ou vrai vrai ;;

applique est_vrai true = applique vrai true ;;
applique est_vrai false = applique vrai false;;

est_vrai = vrai;;




Liaison Caml-Light


La liaison permet de déstructurer des valeurs et d'introduire des identificateurs :

type point = {abs : int ; ord : int} ;;
let ({abs = x ; ord = y} as un_point) = 
  {abs = 10 ; ord = 20 } ;;
Introduit globalement trois nouveaux noms : il n'y a pas de calcul dans les parties gauches d'un let, ce sont des motifs lexicaux. Une telle liaison peut être mal typée ou échouer :
let (a,b) = (1,2,3) in a+b ;;
let (a,1) = (1,2) in a+1 ;;
let projection_verticale = function a_point -> 
  match a_point with
    {abs = x ; ord = y} ->
      if (y = 0) then a_point
      else {abs = x ; ord = 0};;
Le motif ``_'' (souligné) permet de déstructurer sans nommer.
let abscisse = function un_point ->
  match un_point 
    with {abs = x ; ord = _} -> x ;;



Application


Appliquer une fonction f à une valeur x c'est calculer la valeur de f(x).

Il faut évaluer l'expression constituant le corps de la fonction dans l'environnement de définition de la fonction augmenté de la liaison où le paramètre formel est lié à la valeur d'appel (le paramètre effectif).

Soient Fe et Xe deux expressions Caml-Light, la valeur de l'application Fe( Xe) dans l'environnement E est la valeur de la simplification de F(xv) où F est la valeur de Fe dans E et xv est la valeur de Xe dans E.

Si Fx:-> Ex,Ed», alors F(xv) a la même valeur que celle de Ex dans l'environnement ë(x=xv)<|Edû

On peut donc écrire une règle de simplification d'une application :

Fe -E-> («x:-> Ex,Ed»)
Xe -E-> xv
Ex -ë(x=x)<|Edû-> V
Fe( Xe) -E-> V
pour obtenir la valeur d'une application il faut :


Fonctions à plusieurs arguments


Soit E l'environnement courant, a-t-on deux choix ?


Application partielle


Soit Efoo l'environnement de définition de F2.

On peut appliquer F2 à une valeur x0 de type sx.

C'est une application partielle, le résultat en est une autre fermeture :

Fx0= «y:-> E(x,y),ë(x=x0)<|Efooû»

qui a pour type sy ® sE




Fonctions


La construction fun x1 ...xn -> E

est équivalente à function x1 -> ...function xn -> E

L'écriture let F x1 ...xn = E

est équivalente à let F = fun x1 ...xn -> E

La forme où fun ou function est explicite permet de préciser son environnement de définition :

let incremente_compteur = let compteur = ref(0) in
    function n -> 
      begin
      compteur := !compteur + n ;
      !compteur
      end ;;



Le Filtrage




match   Vwith   (| F -> E)

avec la notation

| F -> E =
ì
í
î
| F1 -> E1
...
| Fn -> En

À l'appel, le paramètre effectif V qui est l'évaluation de l'expression V dans l'environnement courant est ``superposé'' avec les filtres Fi dans l'ordre du texte.

On peut donc énoncer une règle pour l'évaluation d'un filtre :
V -E-> V,
$ i (| Fi ® Ei) Î (| F ® E)
et i est le premier indice qui
produise des liaisons Ei de
let Fi = V,
Ei -(Ei Å E)-> R
[match V with (| F ® E)] -E-> R

La construction match V with | F ® E
est équivalente à (function | F ® E) V. On peut ajouter des conditions a une clause de filtrage Fi qui a alors la forme :
| Fiwhen Ci-> Ei

Ci est une expression qui doit être de type bool. Elle est évaluée dans l'environnement d'exécution du corps de la clause :

Si V est le paramètre effectif, et si E est l'environnement courant, Ei et Ci s'évaluent tous deux dans Ei Å E qui est E augmenté des liaisons Ei produites pendant le filtrage.

cette construction est à éviter




La Récursivité


Dans le corps d'un let les identificateurs (noms de fonctions) sont cherchés dans l'environnement courant.

La construction
let rec
introduit un identificateur récursif.

Soit Ec l'environnement courant, la déclaration

let rec foo = E
crée une valeur Vfoo qui est le résultat de l'évaluation de l'expression E dans l'environnement

Efoo=ë(foo=Vfoo)<|Ecû E -Efoo-> Vfoo

et Efoo devient l'environnement courant.

let rec fib = function n -> match n with
  | 0 -> 0
  | 1 -> 1
  | _ -> fib(n-1) + fib(n-2) ;;
définit un environnement Efib, une fermeture Ffibn:-> Efib,Efib»

l'évaluation d'une fonction récursive se fait comme une évaluation normale.

À l'appel d'une fonction récursive, il faut empiler les environnements :




les types Somme


La déclaration
type foo = C | C1 of t1 | C2 of t2
introduit des


les Types Sommes


Sont récursifs :
type somme_formelle =
 | Variable
 | Entier of int
 | Somme of somme_formelle * somme_formelle ;;
let rec eval = function (expr, valeur) -> 
  | (Variable , _ ) -> valeur
  | (Entier n , _ ) -> n
  | (Somme (e1,e2) , _ ) -> 
      eval(e1,valeur) + eval(e2,valeur) ;;
Ensuite :

eval(Somme(Variable,Entier 2) , 4) vaut 6.




Ordre supérieur


Sommer tous les éléments d'une liste ?
let rec somme = function l -> match l with
  | [] -> 0
  | h::r -> h + (somme r) ;;
Ici 0:int et (prefix +):int -> int -> int.
let rec somme_abs =
  function zero -> function plus -> function l ->
    match l with
      | [] -> zero
      | h::r -> 
        (plus h (somme_abs zero plus r)) ;;
On généralise zero: a et plus: b->a->a.
let somme = function l -> somme_abs 0 (prefix +) l;;
Pour tout environnement E si F est un identificateur (monomorphe) tel que

F -E-> F et si

(function x -> F(x)) -E-> G on a :

GºF dans le sens où

" x G(x) = F(x) et

function x -> F(x) Û F

let somme = somme_abs 0 (prefix +) ;;
let mult = somme_abs 1 (prefix *) ;;



Récursion terminale


L'enveloppe d'un appel récursif est l'ensemble des calculs qui doivent d'effectuer après cet appel.

Lors d'un appel récursif, les différents environnements doivent être conservés car d'autres calculs doivent s'exécuter après :
let rec foo = function n -> match n with
  | 0 -> 2
  | _ -> foo(n-1) + n;;
Soit E l'environnement courant, si foo -E-> Ffoo, Ffoon:-> Efoo,Efoo»

Pour évaluer foo(1) dans E, on doit évaluer Ffoo(1) et donc évaluer (foo(n-1) + n) dans E1 =ë(n=1)<|Efooû. Si foo(n-1) est évalué en premier il faut restaurer E1 pour évaluer n.

Un appel récursif est terminal s'il n'y a pas d'autres calculs à effectuer ensuite. Le résultat de la fonction est alors lui même un appel récursif.

let rec foo_bis = 
  function n -> function res -> match n with
    | 0 -> res
    | _ -> foo_bis (n-1) (res + n);;
" xFfoo(x) = (Ffoo_bisx2)

Si (n -En+1-> n) et (res -En+1-> r)

il est inutile de stocker En+1 pour déterminer la valeur de

foo_bis (n-1) (res + n) dans En+1

puisque ((n-1) -En+1-> n-1) et
((res + n) -En+1-> r+n) peuvent être calculés avant


Récursion terminale


Lorsqu'un appel récursif est enveloppé par une opération associative on peut rajouter un paramètre pour obtenir un appel terminal.
let rec foo = function arg ->
  if (arg = arg_0)
  then val_0
  else op(foo(g(arg)), f(arg));;
f et g ne contiennent pas foo, devient lorsque op est associative :
let rec foo = function (arg,accu)
  if (arg = arg_0)
  then op(accu,val_0)
  else foo(g(arg) , op(accu, f(arg)));;



Dichotomie


Calcul de an =
(an/2)2 si n est pair
a(an/2)2 si n est impair
let rec pow = function a -> function n ->
  if ((n mod 2) = 0) 
  then
    if (n = 0) 
      then 1 
      else
        let res = pow a (n / 2) in res * res
  else
    if (n = 1) 
    then a 
    else
      let res = pow a (n / 2) in a * res * res ;;
On peut généraliser 0, 1 et * :
let rec dich = fun zero un op a n ->
  if ((n mod 2) = 0) then
    if (n = 0) 
    then zero 
    else
      let res = dich zero un op a (n/2) in 
        op res res
  else
    if (n = 1) 
    then un 
    else
      let res = dich zero un op a (n/2) in 
        op a (op res res) ;;



Évaluation des arguments


Pour des raisons d'efficacité Caml-Light évalue les arguments d'une application du dernier au premier :

let foo = function x -> function y -> !x + !y ;;

let essai = ref 0 in
     foo   (essai := 1 ; essai)   (essai := 2 ; essai);;
         E1   E2

Ainsi lorsqu'une application est totale il n'y a pas besoin de créer les fermetures intermédiaires :

Fx1x2...xn = F1x2...xn=Fn-1xn




Les exceptions


Caml-Light offre un mécanisme d'erreurs simple grâce au type exn.
exception Foo ;; 
exception Bar of int;;
Les exceptions sont des valeurs :
let to_foo = function an_exn -> match an_exn with
  | Bar _ -> Foo
  | an_exn -> an_exn ;;
marquées exceptionnelles (déclencher ou lever une exception) par  :

raise I

On peut récupérer une exception :
try E with | F ® EI
let v = 1 in
  try (let v = 2 in raise Foo) 
  with
   | Foo -> v 
;;
Soit E l'environnement courant. En particulier les motifs des clauses Fi doivent être des exceptions et les corps des clauses EIi doivent avoir des types compatibles avec sv.

Le filtrage n'a pas besoin d'être exhaustif,

on peut récupérer n'importe quelle exception.

c'est à éviter

Une exception marquée n'est plus une valeur :
let foo = function x -> 
  try (x+1) with 
    | Foo -> -1
;;
foo (raise Foo);;



Traits impératifs


Les références Caml-Light correspondent aux variables des langages impératifs. Ce sont des ``boites'' dans lesquelles on peut ``mettre'' des valeurs de type fixé.

Caml-Light C
let r = ref 1 int r = 1
On distingue la référence de son contenu
r := !r + 1 r = r + 1

On a alors un modèle dynamique de mémoire analogue aux environnements mais dont les ``clés'' sont des références.

L'affectation
(prefix :=):aref ->a -> unit
modifie la valeur associée à la clé, la création
ref :a->aref

Soit M l'état mémoire d'un programme et E l'environnement courant ,

let r = ref v introduit on dit parfois que vr est un pointeur sur v ou l'adresse de v. Si r -E-> vr

Il n'y a pas de masquage dans la mémoire :

é(M|>vr)¬v'ù
=
é(é(vr¬v)<|M'ù|>vr)¬v'ù
=
é(vr¬v')<|M'ù

L'état d'un programme est alors donné par le couple (E, M).

Les vecteurs (vect), enregistrements (record) dont les champs sont déclarés mutables (mutable) suivent le même modèle.

let foo = 
  let c = ref 0 in
    function () -> 
      begin c := !c + 1 ; !c end ;;
incrémente un compteur local à la fonction à chaque appel.

let foo = function () ->
  let c = ref 0 in 
  begin c := !c + 1 ; !c end ;;
Crée un nouveau compteur à chaque appel.

Que fait ? function c -> begin c := !c + 1 ; c end




La séquence, les boucles


l'expression
begin
     E1;
    ...
     En-1;
     En
end
équivaut à
let void = E1 in
    let void = ... in
        let void = En-1 in
             En

La boucle
for i = Im to IM do E done

introduit un nouvel identificateur i : int.

Dans un environnement E, si Im -E-> im et IM -E-> iM, on évalue successivement l'expression E dans les environnements Ei =ë(i=i)<|Eû pour i Î im... iM et on retourne la valeur ().
let i = "i" ;;
for i = 1 to 9 
do 
  begin 
    print_int i ;
    print_newline() ; 
    i 
  end 
done ;;
i;;
l'identificateur i n'est ainsi pas lié en sortie de boucle !




Impératif/Fonctionnel


let fact = function n ->
  let res = ref 1 in
    begin
    for i = 1 to n do
      res := !res * i
    done ; 
    !res
    end;;
let rec fact = function
  | 0 -> 1
  | n -> n * (fact (n-1));;
Consommation de pile !
let fact = 
  let rec f_r = fun r n ->
    match n with
      | 0 -> r
      | _ -> f_r (n*r) (n-1)
  in 
   function n -> f_r 1 n;;
les paramètres n et r sont analogues à i et !res




Impératif/Fonctionnel


let fib = function n ->
  let f_0 = ref(0) 
  and f_1 = ref(1) 
  and temp = ref(0) in
    begin
    for i = 1 to n do
      temp := !f_0;
      f_0 := !f_1;
      f_1 := !temp + !f_1
    done;
    !f_0
    end ;;
let fib =
  let rec f_r = function
    | 0 -> (0, 1)
    | n -> 
      let (f_0,f_1) = 
          f_r(n - 1)
      in (f_1, f_0 + f_1)
  in fun n -> fst(f_r n) ;;
Comment rendre terminal l'appel
f_r(n - 1)




Correction de programmes





Terminaison


Mathématique Caml-Light
Ensemble inductif type récursif
fonction récursive fonction récursive
type arbre_bin = 
  | Feuille of int
  | Noeud of (arbre_bin * arbre_bin) ;;

let rec it_arbre = fun op -> function arbre -> 
  match arbre with
    | Feuille n -> n
    | Noeud (fg,fd) -> 
      op (it_arbre op fg) (it_arbre op fd) ;;
On n'explicite pas les environnements, les expressions, les valeurs !

On identifie les fermetures et leurs noms, les paramètres et leurs valeurs

Par induction on a deux cas :


Correction


let rec append = function l ->
  match l with
    | [] -> function l -> l
    | h::t -> function l -> h::(append t l) ;;
A-t-on l'associativité :
appendl1(appendl2l3) = append(appendl1l2)l3

On conclut par induction en utilisant le résultat sur l1', l2 et l3.




Exemple : le pgcd


let rec gcd = function n -> function m ->
  if (m = 0)
  then n
  else gcd m (n mod m) ;;
Termine pour m et n positifs ? Tout diviseur commun à n et m divise le pgcd :

Si d divise n et m alors n = d n' et m = d m' et on a :

nmodm = (d n')mod(d m')
  = d(n'modm')
en supposant le bon fonctionnement de mod

et d divise (nmodm).


3
sauf si m=n=0 mais le résultat reste correct

Ce document a été traduit de LATEX par HEVEA.