Références et vecteurs

Un article de Mangue.org, l'encyclopéde libre.

Nous allons maintenant présenter les variables, ce qui nous permettra de modifier la valeur d'une donnée aucours de l'évolution de notre programme...


Sommaire

Les constantes

Rappelons rapidement comment les constantes sont stockées en mémoire...

La pile, c'est à dire l'endroit où sont stockées les définitions, n'est pas vide au début de votre session. Celle-ci contient l'environnement de base du Caml. Composé de fonctions que vous utilisez sans même y préter attention comme int_of_string par exemple.
Au fur et à mesure que nous faisons des définitions en Caml, la pile vient s'enrichir de nouvelles liaisons... Ces liaisons codent à la fois le nom, le type et la valeur de vos constantes...

Les différentes définitions sont empilées et vous n'avez aucun moyen de retrouver une ancienne valeur par exemple. Chaque nouvelle définition d'une constante oblitère la précédente en la rendant innaccessible !
Avec les références (ou variables) nous allons voir un nouveau procédé nous permettant d'utiliser des objets dont la valeur n'est pas codée en mémoire. Il ne s'agira plus de définitions comme nous les connaissons... Cette fois-ci une adresse mémoire (et un type) seront associés à un identificateur. Nous pourrons ensuite utiliser à volonté la valeur codée dans cet espace mémoire et même la modifier.

Remarque
Les références ont tout de même un type, celui-ci est nécessaire pour connaître la taille de l'espace mémoire à réserver. Un nombre entier (int) et un nombre réel (float) n'occupent pas du tout le même espace c'est évident.


Nombre d'appels d'une fonction

Nous allons présenter les références au travers d'un exemple. On se propose d'écrire une fonction qui renverra le successur d'un float ainsi que le nombre de fois où elle a été appellée. La fonction renverra donc un couple float, int. Nous aurons besoin d'une valeur pour mémoriser le nombre d'appels de la fonction.
Voilà ce que nous pouvons faire avec une constante :

# let nb_appels = 0 ;;
nb_appels : int = 0
# let f(x) =
  let nb_appels = nb_appels + 1 in
  (x +. 1.0, nb_appels) ;;
f : float -> float * int = <fun>
# f(1.0) ;;
- : float * int = 2.0, 1
# f(-.2e3) ;;
- : float * int = -1999.0, 1

Après un premier essai encourageant notre fonction n'a pas réalisé le traitement que nous désirions lors de son second appel. Et pour cause, nous utilsons une définition locale, ce qui implique que la valeur de nb_appels ne variera pas hors de l'évaluation de la fonction et restera toujours a 0.

Les références vont nous permettre de modifier une valeur aucours des appels de la fonction. Pour définir des références en Caml, il faut utiliser ref comme le démontrent les exemples suivants :

# let entier = ref 0
  and réel = ref 0.0 ;;
entier : int ref = ref 0
réel : float ref = ref 0.0
# entier ;;
- : int ref = ref 0
#réel ;;
- : float ref = ref 0.0
# int_of_float(réel) ;;
Toplevel input:
> int_of_float(réel) ;;
>              ^^^^
This expression has type float ref,
but is used with type float.
# !entier ;;
- : int = 0
# !réel +. float_of_int(!entier + 1) ;;
- : float = 1.0

Vous pouvez définir des références de n'importe quel type. Il est possible, par exemple, de définir une référence de type ( 'a -> 'a -> bool) ref même si vous avez peu de chances de l'utiliser un jour dans un de vos programmes.

Danger
Attention à ne pas oublier les parenthèses autour de l'expression que vous souhaitez affecter à votre nouvelle référence sans quoi le compilateur génèrera une erreur.
let a = ref 1 + 2 est incorrect alors que let a = ref (1 + 2) sera évalué correctement !

Vous le voyez, lorsque nous demandons l'évaluation d'une référence, le résultat renvoyé est de type 'a ref. Pour n'avoir que la valeur d'une référence au moment de son évaluation, il faut utiliser l'opérateur ! qui s'utilise en notation infixe (c'est à dire qui se place avant l'identificateur de la référence).
a ;; nous retournera la référence a alors que !a ;; nous retournera la valeur de la référence a.

Remarque
Attention, si vous êtes habitués à des langages dont la syntaxe est basée sur celle du C, comme le C++, le Java, le C#, le PHP et j'en passe (pour ne citer que les plus courants). Ne confondez pas l'opérateur ! avec la négation logique qui est représentée par la fonction not en Caml.


Utilisation des références

C'est bien beau de pouvoir définir des références, mais comment va-t-on pouvoir en modifier les valeurs aucours de l'exécution du programme ? Puisque c'est tout de même ça qui nous intéresse.
Il faudra simple utiliser l'opérateur := emprunté au Pascal (ou à l'Ada)

# let d = ref 0.0 ;;
d : float ref = ref 0.0
# !d ;;
- : float = 0.0
# d := 1.5 ;;
- : unit = ()
# !d ;;
- : float = 1.5
# d := !d +. 1. ;;
- : unit = ()
# !d ;;
- : float = 2.5

La valeur a bien changée, mais il ne s'agit en aucun cas d'une redéfinition, ne confondez pas !

Maintenant que nous savons utiliser les références, nous allons enfin pouvoir écrire notre fonction f qui renvoie le successeur d'une nombre flottant ainsi que le nombre d'appels...
Pour éviter que la référence comptabilisant le nombre d'appels de la fonction soit modifiée accidentellement dans le reste du programme (ce qui fausserait notre résultat vous n'en doutez pas!), nous allons définir cette variable localement à notre fonction. Voici une correction possible :

# let f =
  let cpt = ref 0
 in
  fun x ->
    incr (cpt); (* équivalent de cpt := !cpt + 1 *)
    (x +. 1.0, !cpt) ;;
f : float -> float * int = <fun>
# f(0.0) ;;
- : float * int = 1.0, 1
# f(1.0) ;;
- : float * int = 2.0, 2
# f(3.0) ;;
- : float * int = 4.0, 3

Astuce
Dans la définition de cette fonction vous voyez apparaître, pour la première fois, un unique ;
Celui-ci permet de séparer des expressions renvoyant un résultat de type unit. Vous en apprendrez plus sur ce procédé dans le cours suivant. Pour l'instant considérez simplement que incr(cpt) est évalué avant de renvoyer le résultat de la fonction.


Références et addresses mémoires

Considérez l'exemple suivant dans lequel nous définissons des références de plusieurs manières :

# let x = ref 7 ;;
x : int ref = ref 7
# let y = ref (!x) ;;
y : int ref = ref 7
# let z = x ;;
z : int ref = ref 7

Vous serrez certainement d'accords si je vous annonçait qu'à la fin de ce programme les trois références x, y et z ont toutes les trois la même valeur, 7. Mais y a-t-il une différence entre ces références ?

Oui il y a une différence, x est la première référence servant à définir les deux autres. Cependant les plus observateurs d'entre vous auront remarqué une différence fondamentale, y est créée à partir de la valeur de la référence x, concrètement celà revient à un let y = ref 7 alors que z est directement une copie de x. Ce schéma vous aidera certainement à mieux comprendre :

Astuce
Puisque x et z pointent sur la même adresse en mémoire, la modification de la valeur pointée par x entrainera la modification de la valeur pointée par z (c'ets la même!)


Exercice

Nous devons écrire une fonction echange chargée de permutter les valeurs pointées par deux références. Vous avez une correction de cet exercice dans la page correspondante. Voici un exemple de son exécution :

# let a,b = ref 0, ref 1 ;;
a : int ref = ref 0
b : int ref = ref 1
# echange a b ;;
- : unit = ()
# !a ;;
- : int = 1
# !b ;;
- : int = 0


Présentation des vecteurs

Les vecteurs sont des suites ordonnées et homogènes de valeurs du même type. C'est une structure très utilisée en Caml et si elle n'a pas été présentée avant c'est tout simplement parce que ses valeurs sont modifiables, comme les références.

Les vecteurs peuvent être construits de plusieurs manières différentes, la première consiste à utiliser la fonction make_vect qui, comme vous pourrrez le lire dans l'aide, prends en argument le nombre d'éléments du vecteur puis sa valeur d'initialisation. make_vect fonctionne un peu de la même manière que make_string.
L'autre manière de construire des vecteurs est d'énumérer ses valeurs entre [| et |] et séparées par des ; comme dans l'exemple suivant :

# let vect1 = [| 0; 1; 2; 3 |]
  and vect2 = make_vect 4 make_string
(* make_string est une valeur d'initialisation de type int->char->string *)
;;
vect1 : int vect = [|0; 1; 2; 3|]
vect2 : (int -> char -> string) vect = [|<fun>; <fun>; <fun>; <fun>|]

Remarque
Remarquez qu'un vecteur peut, tout comme une liste, être vide. L'évaluation de [] ;; produira donc un résultat de type 'a vect

Pour accéder à un élément d'un vecteur il suffit de l'indexer, vecteur.(rang), vous pouvez aussi modifier la valeur du dit élément en utilisant l'opérateur <-
Mais les exemples suivants vous seront certainement plus utiles pour cerner avec précision le problème :

# let vecteur = [| 0; 1; 2; 3 |] ;;
vecteur : int vect = [|0; 1; 2; 3|]
# vect_length (vecteur) ;;
- : int = 4
# vecteur.(0) ;;
- : int = 0
# vecteur.(4) ;;
Uncaught exception: Invalid_argument "vect_item"
# vecteur.(0) <- 3;
  vecteur.(1) <- 2;
  vecteur.(2) <- 1;
  vecteur.(3) <- 0 ;;
- : unit = ()
#vecteur ;;
- : int vect = [|3; 2; 1; 0|]

Pour l'instant vous n'avez que peu d'intérêt à utliser les vecteurs dans vos programmes. En effet la seule solution pour les exploiter est d'implémenter des fonctions récursives qui agiront sur chaque élément du dit vecteur.
En revanche dès que nous présenterons les itérations vous comprendrez à quel point ceux-ci sont utiles. Nous construirons également des vecteurs de vecteurs, et plus encore !


Vecteurs et adresses mémoires

Vous avez vu avec les références que si les précautions nécessaires n'étaient pas prises, il était possible de modifier par mégarde une donnée en mémoire. Et bien le problème demeure avec les vecteurs.
Si un vecteur est défini directement par rapport à l'autre, let vecteur2 = vecteur1 ;;, et bien les deux vecteurs désigneront le même espace en mémoire. Des modifications sur le premier influeront sur le second.

# let t = [| 0; 1; 2; 3 |] ;;
t : int vect = [|0; 1; 2; 3|]
# let t' = t ;;
t' : int vect = [|0; 1; 2; 3|]
# t.(0) <- 5 ;
  t ;;
- : int vect = [|5; 1; 2; 3|]
# t' ;;
- : int vect = [|5; 1; 2; 3|]

Remarque
Les vecteurs sont, comme les références, des pointeurs sur des adresses mémoires. A la différence qu'ils pointent sur plusieurs cases consécutives.


Utilisation des vecteurs

Pour le moment vous ne pouvez pas aller beaucoup plus loin dans l'utilisation des vecteurs. Mais nous verrons bientôt comment provoquer des itérations en Caml. Ce qui est particulièrement adapté au traitement de telles structures.
Si vous souhaitez vous entrainer sur les vecteurs essayez donc d'écrire un fonction qui fera la somme de tous les éléments d'un vecteur d'entiers. Pour l'instant vous n'avez d'autre choix que d'implémenter une fonction récursive. La correction est donnée dans la page correspondante.

# somme (vecteur) ;;
- : int = 6
# somme [| 0; 1; -1; 2; 3 |] ;;
- : int = 5

Outils personels