Programmation Fonctionnelle Avancée

Cours 6

kn@lri.fr
http://www.lri.fr/~kn

Corrigé commenté du TP

Corrigé commenté du TP 5

On fait un corrigé commenté du TP (~15 à 20 minutes)

Traits impératifs du langage

Programmation impérative

Au niveau le plus bas, un ordinateur est une machine impérative :

Pour calculer sur un ordinateur, il faut donc, à un moment, faire des effets de bords. Nous allons voir :

Références

Référence

La notion la plus simple est la notion de référence représentée par le type 'a ref. Il s'agit d'une case mémoire modifiable dont le contenu est de type 'a. Les opérations sont :

let x = ref 42 let () = Printf.printf "%d\n" !x; (* affiche 42 *) x := !x + 1; Printf.printf "%d\n" !x (* affiche 43 *)

Utilité

On peut vouloir créer une fonction uid() qui renvoie un entier unique (ex: créer des noms de fichiers distincts, donner des IDs unique à des données, …)

let cpt = ref 0 let uid () = let c = !cpt in cpt := c + 1; c let x = uid () (* 0 *) let y = uid () (* 1 *) let z = uid () (* 2 *)

Quel problème potentiel avec cette fonction ?

La référence est accessible, du code erroné pourrait faire:

let () = cpt := 0 (* réinitialisation! le compteur n'est plus unique *)

Peut-on faire mieux ?

Référence locale

On peut utiliser un let in

let uid = let cpt = ref 0 in fun () -> let c = !cpt in cpt := c + 1; c

C'est un exemple simple de coopération entre programmation impérative (une référence) et fonctionnelle (on définit une fonction en plein milieu d'une expression, ici un let … in)

Références et ordre d'évaluation

Attention, l'ordre d'évaluation en OCaml n'est spécifié que pour certaines expressions :

Dans tous les autres cas, l'ordre n'est pas spécifié !

let x = ref 0 let a, b = (!x, (x:= !x+1; !x)) (* a ? b ? *) (* on suppose que l'on a encore jamais appelé uid() *) let c = (uid ()) - (uid ()) (* 1 - 0 ou 0 - 1 ? *)

Ne jamais écrire un tel code ! Les résultats dépendent d'un ordre d'évaluation non spécifié, l'ordre peut changer selon les versions d'OCaml.

Enregistrements mutables

Enregistrements mutables

(* définition de type *) type r = { l1: t1; …; ln : tn } (* définition de valeur *) let r = { l1 = e1; …; ln = en }

Dans la définition de type, on peut qualifier un champ de mutable pour dire qu'on peut le mettre à jour.

type point = { mutable x : float; mutable y : float } let move p i j = p.x <- p.x +. i; p.y <- p.y +. j let p = { x = 0.0; y = 0.0 } let () = move p 1.0 2.0 let () = Printf.printf "%f, %f\n" p.x p.y (* 1.0, 2.0 *)

Référence vs enregistrement mutable ?

Les références ne sont qu'un cas particulier d'enregistrement mutable

type 'a ref = { mutable contents : 'a } let ref x = { contents = x }

Attention au partage

let x = ref 0 (* création d'une ref *) let y = ref 0 (* création d'une autre ref *) let z = x (* x et z sont la même ref *) let () = x := 1; Printf.printf "%d, %d, %d\n" !x !y !z (* affiche 1, 0, 1 *)

L'utilisation des structures mutable permet d'observer le partage des objets en mémoire, chose qu'on ne pouvait pas faire en prog. fonctionnelle pure.

Nous reviendrons sur cet aspect qui est source de bug.

Tableaux

type 'a array

OCaml possède un type pour les tableaux de taille fixe pour des éléments de type 'a

let tab = [| 1; 5; 42; -3 |] let x = tab.(0) let () = tab.(0) <- x + 1; Printf.printf "%d\n" tab.(0) (* affiche 2 *)

Comme pour les listes, les tableaux sont homogènes (on ne peut pas mélanger d'éléments de plusieurs types différents dans le même tableau)

Le module Array

Les fonctions sur les tableaux sont regroupées dans le module Array. Ce module regroupe

Créations de tableaux et fonctions de base

Itérateurs

Tout ceux qu'on aime connaît :

Tri de tableau

La fonction de tri est :

Array.sort : ('a -> 'a -> int) -> 'a array -> unit (* Tri en place, le tableau est modifié par le tri *)

C'est une différence significative d'API avec les listes, ici le tableau est modifié. Si on veut conserver le tableau original, il faut utiliser Array.copy

Attention au partage !

let tab = Array.make 3 [| 0; 0; 0 |] (* une "matrice" de taille 3x3 ? *) let pr_tab t = Array.iter (fun s -> Array.iter (fun i -> Printf.printf "%d " i) s; Printf.printf "\n") t let () = tab.(0).(0) <- 42 let () = pr_tab tab (* 42 0 0 42 0 0 42 0 0 C'est le même tableau interne qui est utilisé 3 fois ! On écrira plutôt : *) let tab2 = Array.init 3 (fun _ -> Array.make 3 0) (* la fonction est rappelée pour chaque case, un nouveau tableau est alloué à chaque fois *)

Boucles

Boucle while

La boucle while est classique. On utilise les mots clés doet done pour délimiter le corp dela boucle:

let res = ref 0 let i = ref 0 let () = while !i < 10 do res := !res + i; i := !i + 1 done

Comme la boucle doit utiliser une condition qui change (sinon c'est une boucle infinie), on doit utiliser des effets de bords pour modifier les valeurs testées (ici on modifie la référence i)

Boucle for

La boucle for permet d'aller d'une borne à une autre par pas de 1:

let () = for i = 0 to 42 do Printf.printf "%d\n" i; done; for j = 100 downto 0 do Printf.printf "%d\n" j; done

Exemple de boucle for et tableau

let average tab = let total = ref 0.0 in for i = 0 to Array.length tab - 1 do total := !total +. tab.(i); done; !total /. (float (Array.length tab)) (* average: float array -> float *)

Pas de break :(

Le langage OCaml ne possède pas les mots clés break et continue. 😭

On peut les simuler grâce a des exceptions. 🤔

exception Break exception Continue let () = try for i in 0 to 100 do try ... (* le code ici peut contenir raise Continue ou raise Break *) with Continue -> () done with Break -> ()

C'est pour montrer que c'est possible, mais en pratique on utilise plutôt les itérateurs prédéfinis qui s'arrêtent au bon moment.

Conclusion