L'unique valeur rien a le type prédéfini unit; elle est notée () et est lue ``voïde''. La valeur rien sert à compléter une conditionnelle à une seule branche (par else ()), à déclencher les procédures (print_newline ()), et comme instruction vide dans le corps d'une boucle.
Les booléens ont le type prédéfini bool, qui contient deux constantes true et false.
Les entiers ont le type prédéfini int. Les constantes entières sont une suite de chiffres décimaux, éventuellement précédée d'un signe -, comme 234, -128, ...Les valeurs extrémales dépendent de l'implémentation.
Les flottants ont le type prédéfini float. Les constantes flottantes sont une suite de chiffres décimaux comprenant un point, éventuellement suivie d'une indication d'exposant, comme 3.1416 ou 3141.6E-3 pour désigner 3141,6 × 10-3.
Les caractères ont le type prédéfini char. Une constante caractère est une lettre entourée du symbole `, comme `a`, `b`, ..., `+`, `:`. Certains caractères sont codés spécialement comme `\\n` pour le changement de ligne, `\\r` pour le retour charriot, `\\t` pour la tabulation, `\\`` pour le caractère apostrophe, et `\\\\` pour la barre oblique. Enfin, on dénote n'importe quel caractère en le désignant par son numéro décimal dans le code ASCII (American Standard Codes for Information Interchange) (ISO-latin), sur trois chiffres et précédé d'une barre oblique. Ainsi `\\032` désigne le caractère espace et `\\233` est équivalent à `é`. La fonction int_of_char donne la valeur entre 0 et 255 dans le code ASCII du caractère. Inversement, la fonction char_of_int donne le caractère par son code ASCII.
Les chaînes de caractères ont le type prédéfini string. Ce sont des suites de caractères rangés consécutivement en mémoire. Une constante chaîne est une suite de caractères entourée de guillemets. Dans une chaîne, le caractère guillemet se note \\" en ajoutant une barre oblique devant le guillemet, et les caractères spéciaux obéissent aux mêmes conventions que pour les constantes caractères. Les éléments d'une chaîne de caractères sont numérotés à partir de 0. On accède à l'élément i de la chaîne s à l'aide de la notation s.[i]. On remplace l'élément i de la chaîne s par la valeur c, à l'aide de la notation s.[i] <- c. En évaluant make_string l c, on obtient une chaîne de caractères de longueur l, initialisée avec des caractères c. L'opérateur infixe ^ sert à concaténer deux chaînes, la fonction sub_string permet d'extraire une sous-chaîne, et la procédure blit_string de transférer des caractères d'une chaîne à une autre. Pour plus d'information, voir le module string de la librairie.
Les vecteurs ont le type prédéfini vect. Ce sont des suites d'éléments de même type, rangés consécutivement en mémoire. Une constante vecteur est une suite d'éléments séparés par des ; et entourée de ``parenthèses'' [| et |]. Par exemple:
#let v = [| 1; 2; 3 |];;
v : int vect = [|1; 2; 3|]
Remarquons la notation suffixe pour le constructeur de type des vecteurs. Le type d'un vecteur d'entiers s'écrit int vect, et le type d'une matrice d'entiers int vect vect. Les éléments d'un vecteur sont numérotés à partir de 0. On accède à l'élément i du vecteur v à l'aide de la notation v.(i). On remplace l'élément i du vecteur v par la valeur c, à l'aide de la notation v.(i) <- c. En évaluant make_vect l c, on obtient un vecteur de longueur l, initialisé avec l'élément c. On dispose aussi des fonctions sub_vect pour extraire des sous-chaînes et blit_vect pour transférer des éléments d'un vecteur à un autre. Pour plus d'information, voir le module vect de la librairie.
Les références ont le type prédéfini ref. Comme les vecteurs le constructeur de type des références ref utilise la notation suffixe. Une référence est construite par l'application du constructeur (de valeur) ref à sa valeur initiale. En évaluant ref v, on obtient une référence, initialisée avec la valeur v. On accède au contenu d'une référence r à l'aide de la notation !r. On remplace le contenu de la référence r par la valeur c, à l'aide de la notation r := c.
Les paires d'éléments de type t1 et t2 ont le type t1 * t2. On écrit la paire des valeurs v1 et v2 de la manière mathématique classique: (v1, v2). La notation s'étend aux n-uplets. Il n'y a pas de limitation à l'usage des n-uplets, qui peuvent être librement pris en argument et rendus en résultat des fonctions. Par exemple, la symétrie par rapport à l'origine du repère s'écrit: #let symétrie (x, y) = (-x, -y);;
symétrie : int * int -> int * int =
Attention, les n-uplets ne sont pas associatifs et (1, 2, 3) ¹ ((1, 2), 3).
B.2.4 Expressions
Les expressions arithmétiques font intervenir les opérateurs classiques sur les entiers + (addition), - (soustraction), * (multiplication), / (division entière), mod (modulo). On utilise les parenthèses comme en mathématiques. Ainsi, si x et y sont deux entiers, on écrit 3 * (x + 2 * y) + 2 * x * x pour 3 (x + 2y) + 2 x2.
Les mêmes opérateurs, suffixés par un point, servent pour les expressions flottantes. Donc, si z est un flottant, on écrit 3.0 *. (z +. 1) /. 2.0 pour 3 (z+1) / 2. Les fonctions int_of_float et float_of_int autorisent les conversions des flottants dans les entiers: la première donne la partie entière, la seconde convertit un entier en flottant. Contrairement à Pascal ou à C, les conversions ne sont jamais automatiques: par exemple 3.5 + 2 est toujours mal typé.
if e then e1 else e2
où la condition e est une expression booléenne du type bool, et e1, e2 sont deux expressions de même type qui est celui du résultat.
Les expressions booléennes sont construites à partir des opérateurs ||, &&, not, des booléens et des opérateurs de comparaison. Ainsi, si b et c sont deux identificateurs de type bool, l'expression(b && not c) || (not b && c)
représente le ou-exclusif de b et c. Les deux opérateurs || et && se comportent exactement comme une construction ``if then else''. Par définition, a && b signifie if a then b else false et a || b signifie if a then true else b. Parfois ces opérateurs rendent un résultat sans évaluer certains de leurs arguments. Si a s'évalue en faux, alors a && b rend false sans que l'expression b soit évaluée. Les opérateurs de comparaison =, <>, <=, <, >, >= rendent aussi des valeurs booléennes. On peut comparer des entiers, des flottants, des booléens, des caractères (dans ce dernier cas, l'ordre est celui du code ASCII) et même deux valeurs quelconques, pourvu qu'elles soient du même type.
La précédence des opérateurs est naturelle. Ainsi * est plus prioritaire que +, lui-même plus prioritaire que =. Si un doute existe, il ne faut pas hésiter à mettre des parenthèses. De manière générale, seules les parenthèses vraiment significatives sont nécessaires. Par exemple, dans
if (x > 1) && (y = 3) then ...
la signification ne change pas si l'on ôte toutes les parenthèses. De même, dans l'expression du ou-exclusif (b && not c) || (not b && c)
les parenthèses sont superflues. En effet les précédences respectives de &&, || et not sont analogues à celles de *, + et -. On écrit donc plus simplement b && not c || not b && c. (Encore plus simple: b <> c!) Évidemment, certaines parenthèses sont impératives pour grouper les expressions. L'exemple des arguments de fonctions est plus particulièrement fréquent: comme en mathématiques f (x + 1) est essentiellement différent de f (x) + 1. Et comme on omet souvent les parenthèses autour des arguments très simples (variables ou constantes), il faut aussi noter que f (x) + 1 est synonyme de f x + 1. De toutes façons, les parenthèses sont indispensables pour les arguments de fonctions compliqués. Pour la même raison les parnthèses sont nécessaires autour des arguments négatifs f (-1) ¹ f -1, car f -1 est synonyme de f - 1 qui est une soustraction.
f (x + 1) ¹ f x + 1.
L'ordre d'évaluation des opérateurs dans les expressions respecte les conventions mathématiques lorsqu'elles existent (priorité de l'addition par rapport à la multiplication par exemple). En ce qui concerne l'application des fonctions, on évalue les arguments avant de rentrer dans le corps de la fonction (appel par valeur). Comme en C, il n'y a pas d'appel par référence mais on peut pratiquement le simuler en utilisant des références (c'est le cas pour la fonction decr décrite page X). L'ordre d'évaluation des arguments des opérateurs et des fonctions n'est pas spécifié par le langage. C'est pourquoi il faut impérativement éviter de faire des effets dans les arguments de fonctions. En règle générale, il ne faut pas mélanger les effets (impressions, lectures ou modification de la mémoire, déclenchement d'exceptions) avec l'évaluation au sens mathématique.
#"ok" = "ok";;
- : bool = true
#"ok" == "ok";;
- : bool = false
L'égalité physique est indispensable pour comparer directement les références, plutôt que leur contenu (ce que fait l'égalité structurelle). On s'en sert par exemple dans les algorithmes sur les graphes. #let x = ref 1;;
x : int ref = ref 1
#let y = ref 1;;
y : int ref = ref 1
#x = y;;
- : bool = true
#x == y;;
- : bool = false
#x == x;;
- : bool = true
#x := 2;;
- : unit = ()
#x = y;;
- : bool = false
B.2.5 Blocs et portée des variables
Dans le corps des fonctions, on définit souvent des valeurs locales pour calculer le résultat de la fonction. Ces définitions sont introduites par une construction let ident = expression in .... Il n'y a pas de restriction sur les valeurs locales, et les définitions de fonctions sont admises. Ces fonctions locales sont elles-mêmes susceptibles de comprendre de nouvelles définitions de fonctions si nécessaire et ce ad libitum.
Lorsqu'on cite un identificateur x dans un programme, il fait nécessairement référence au dernier identificateur de nom x lié par une définition let, ou introduit comme paramètre d'une fonction après le mot-clé function. En général, il est plus élégant de garder les variables aussi locales que possible et de minimiser le nombre de variables globales. Ce mode de liaison des identificateurs (qu'on appelle la portée statique) est surprenant dans le système interactif. En effet, on ne peut jamais modifier la définition d'un identificateur; en particulier, la correction d'une fonction incorrecte n'a aucun effet sur les utilisations antérieures de cette fonction dans les fonctions déjà définies.
let successeur x = x - 1;;
let plus_deux x = successeur (successeur x);;
#plus_deux 1;;
- : int = -1
Ici, on constate la bévue dans la définition de successeur, on corrige la fonction, mais cela n'a aucun effet sur la fonction plus_deux. #let successeur x = x + 1;;
successeur : int -> int =
#plus_deux 1;;
- : int = -1
La solution à ce problème est de recharger complètement les fichiers qui définissent le programme. En cas de doute, quitter le système interactif et recommencer la session.
B.2.6 Correction des programmes
Le suivi de l'exécution des fonctions est obtenu à l'aide du mode trace qui permet d'afficher les arguments d'une fonction à l'entrée dans la fonction et le résultat à la sortie. Dans l'exemple du paragraphe précédent, le mécanisme de trace nous renseigne utilement: en traçant la fonction successeur on constate qu'elle n'est jamais appelée pendant l'évaluation de plus_deux 1 (puisque c'est l'ancienne version de successeur qui est appelée). #trace "successeur";;
La fonction successeur est dorénavant tracée.
- : unit = ()
#successeur 1;;
successeur <-- 1
successeur --> 2
- : int = 2
#plus_deux 1;;
- : int = -1
Le mode trace est utile pour suivre le déroulement des calculs, mais moins intéressant pour pister l'évolution de la mémoire. En ce cas, on imprime des messages pendant le déroulement du programme.
Les booléens ont le type prédéfini bool, qui contient deux constantes true et false.
Les entiers ont le type prédéfini int. Les constantes entières sont une suite de chiffres décimaux, éventuellement précédée d'un signe -, comme 234, -128, ...Les valeurs extrémales dépendent de l'implémentation.
Les flottants ont le type prédéfini float. Les constantes flottantes sont une suite de chiffres décimaux comprenant un point, éventuellement suivie d'une indication d'exposant, comme 3.1416 ou 3141.6E-3 pour désigner 3141,6 × 10-3.
Les caractères ont le type prédéfini char. Une constante caractère est une lettre entourée du symbole `, comme `a`, `b`, ..., `+`, `:`. Certains caractères sont codés spécialement comme `\\n` pour le changement de ligne, `\\r` pour le retour charriot, `\\t` pour la tabulation, `\\`` pour le caractère apostrophe, et `\\\\` pour la barre oblique. Enfin, on dénote n'importe quel caractère en le désignant par son numéro décimal dans le code ASCII (American Standard Codes for Information Interchange) (ISO-latin), sur trois chiffres et précédé d'une barre oblique. Ainsi `\\032` désigne le caractère espace et `\\233` est équivalent à `é`. La fonction int_of_char donne la valeur entre 0 et 255 dans le code ASCII du caractère. Inversement, la fonction char_of_int donne le caractère par son code ASCII.
Les chaînes de caractères ont le type prédéfini string. Ce sont des suites de caractères rangés consécutivement en mémoire. Une constante chaîne est une suite de caractères entourée de guillemets. Dans une chaîne, le caractère guillemet se note \\" en ajoutant une barre oblique devant le guillemet, et les caractères spéciaux obéissent aux mêmes conventions que pour les constantes caractères. Les éléments d'une chaîne de caractères sont numérotés à partir de 0. On accède à l'élément i de la chaîne s à l'aide de la notation s.[i]. On remplace l'élément i de la chaîne s par la valeur c, à l'aide de la notation s.[i] <- c. En évaluant make_string l c, on obtient une chaîne de caractères de longueur l, initialisée avec des caractères c. L'opérateur infixe ^ sert à concaténer deux chaînes, la fonction sub_string permet d'extraire une sous-chaîne, et la procédure blit_string de transférer des caractères d'une chaîne à une autre. Pour plus d'information, voir le module string de la librairie.
Les vecteurs ont le type prédéfini vect. Ce sont des suites d'éléments de même type, rangés consécutivement en mémoire. Une constante vecteur est une suite d'éléments séparés par des ; et entourée de ``parenthèses'' [| et |]. Par exemple:
#let v = [| 1; 2; 3 |];;
v : int vect = [|1; 2; 3|]
Remarquons la notation suffixe pour le constructeur de type des vecteurs. Le type d'un vecteur d'entiers s'écrit int vect, et le type d'une matrice d'entiers int vect vect. Les éléments d'un vecteur sont numérotés à partir de 0. On accède à l'élément i du vecteur v à l'aide de la notation v.(i). On remplace l'élément i du vecteur v par la valeur c, à l'aide de la notation v.(i) <- c. En évaluant make_vect l c, on obtient un vecteur de longueur l, initialisé avec l'élément c. On dispose aussi des fonctions sub_vect pour extraire des sous-chaînes et blit_vect pour transférer des éléments d'un vecteur à un autre. Pour plus d'information, voir le module vect de la librairie.
Les références ont le type prédéfini ref. Comme les vecteurs le constructeur de type des références ref utilise la notation suffixe. Une référence est construite par l'application du constructeur (de valeur) ref à sa valeur initiale. En évaluant ref v, on obtient une référence, initialisée avec la valeur v. On accède au contenu d'une référence r à l'aide de la notation !r. On remplace le contenu de la référence r par la valeur c, à l'aide de la notation r := c.
Les paires d'éléments de type t1 et t2 ont le type t1 * t2. On écrit la paire des valeurs v1 et v2 de la manière mathématique classique: (v1, v2). La notation s'étend aux n-uplets. Il n'y a pas de limitation à l'usage des n-uplets, qui peuvent être librement pris en argument et rendus en résultat des fonctions. Par exemple, la symétrie par rapport à l'origine du repère s'écrit: #let symétrie (x, y) = (-x, -y);;
symétrie : int * int -> int * int =
Attention, les n-uplets ne sont pas associatifs et (1, 2, 3) ¹ ((1, 2), 3).
B.2.4 Expressions
Les expressions arithmétiques font intervenir les opérateurs classiques sur les entiers + (addition), - (soustraction), * (multiplication), / (division entière), mod (modulo). On utilise les parenthèses comme en mathématiques. Ainsi, si x et y sont deux entiers, on écrit 3 * (x + 2 * y) + 2 * x * x pour 3 (x + 2y) + 2 x2.
Les mêmes opérateurs, suffixés par un point, servent pour les expressions flottantes. Donc, si z est un flottant, on écrit 3.0 *. (z +. 1) /. 2.0 pour 3 (z+1) / 2. Les fonctions int_of_float et float_of_int autorisent les conversions des flottants dans les entiers: la première donne la partie entière, la seconde convertit un entier en flottant. Contrairement à Pascal ou à C, les conversions ne sont jamais automatiques: par exemple 3.5 + 2 est toujours mal typé.
Une expression conditionnelle ou alternative s'écrit:
En Caml, les conversions sont explicites.
if e then e1 else e2
où la condition e est une expression booléenne du type bool, et e1, e2 sont deux expressions de même type qui est celui du résultat.
Les expressions booléennes sont construites à partir des opérateurs ||, &&, not, des booléens et des opérateurs de comparaison. Ainsi, si b et c sont deux identificateurs de type bool, l'expression(b && not c) || (not b && c)
représente le ou-exclusif de b et c. Les deux opérateurs || et && se comportent exactement comme une construction ``if then else''. Par définition, a && b signifie if a then b else false et a || b signifie if a then true else b. Parfois ces opérateurs rendent un résultat sans évaluer certains de leurs arguments. Si a s'évalue en faux, alors a && b rend false sans que l'expression b soit évaluée. Les opérateurs de comparaison =, <>, <=, <, >, >= rendent aussi des valeurs booléennes. On peut comparer des entiers, des flottants, des booléens, des caractères (dans ce dernier cas, l'ordre est celui du code ASCII) et même deux valeurs quelconques, pourvu qu'elles soient du même type.
La précédence des opérateurs est naturelle. Ainsi * est plus prioritaire que +, lui-même plus prioritaire que =. Si un doute existe, il ne faut pas hésiter à mettre des parenthèses. De manière générale, seules les parenthèses vraiment significatives sont nécessaires. Par exemple, dans
if (x > 1) && (y = 3) then ...
la signification ne change pas si l'on ôte toutes les parenthèses. De même, dans l'expression du ou-exclusif (b && not c) || (not b && c)
les parenthèses sont superflues. En effet les précédences respectives de &&, || et not sont analogues à celles de *, + et -. On écrit donc plus simplement b && not c || not b && c. (Encore plus simple: b <> c!) Évidemment, certaines parenthèses sont impératives pour grouper les expressions. L'exemple des arguments de fonctions est plus particulièrement fréquent: comme en mathématiques f (x + 1) est essentiellement différent de f (x) + 1. Et comme on omet souvent les parenthèses autour des arguments très simples (variables ou constantes), il faut aussi noter que f (x) + 1 est synonyme de f x + 1. De toutes façons, les parenthèses sont indispensables pour les arguments de fonctions compliqués. Pour la même raison les parnthèses sont nécessaires autour des arguments négatifs f (-1) ¹ f -1, car f -1 est synonyme de f - 1 qui est une soustraction.
f (x + 1) ¹ f x + 1.
L'ordre d'évaluation des opérateurs dans les expressions respecte les conventions mathématiques lorsqu'elles existent (priorité de l'addition par rapport à la multiplication par exemple). En ce qui concerne l'application des fonctions, on évalue les arguments avant de rentrer dans le corps de la fonction (appel par valeur). Comme en C, il n'y a pas d'appel par référence mais on peut pratiquement le simuler en utilisant des références (c'est le cas pour la fonction decr décrite page X). L'ordre d'évaluation des arguments des opérateurs et des fonctions n'est pas spécifié par le langage. C'est pourquoi il faut impérativement éviter de faire des effets dans les arguments de fonctions. En règle générale, il ne faut pas mélanger les effets (impressions, lectures ou modification de la mémoire, déclenchement d'exceptions) avec l'évaluation au sens mathématique.
L'opérateur d'égalité s'écrit avec le symbole usuel =. C'est un opérateur polymorphe, c'est-à-dire qu'il s'applique sans distinction à tous les types de données. En outre, c'est une égalité structurelle, c'est-à-dire qu'elle parcourt complètement ses arguments pour détecter une différence ou prouver leur égalité. L'habitué de C peut être surpris, si par mégarde il utilise le symbole == au lieu de =, car il existe aussi un opérateur == en Caml (et son contraire !=). Cet opérateur teste l'égalité physique des valeurs (identité des adresses mémoire en cas de valeurs allouées). Deux objets physiquement égaux sont bien sûr égaux. La réciproque n'est pas vraie:
En Caml, l'ordre d'évaluation des arguments n'est pas spécifié.
#"ok" = "ok";;
- : bool = true
#"ok" == "ok";;
- : bool = false
L'égalité physique est indispensable pour comparer directement les références, plutôt que leur contenu (ce que fait l'égalité structurelle). On s'en sert par exemple dans les algorithmes sur les graphes. #let x = ref 1;;
x : int ref = ref 1
#let y = ref 1;;
y : int ref = ref 1
#x = y;;
- : bool = true
#x == y;;
- : bool = false
#x == x;;
- : bool = true
#x := 2;;
- : unit = ()
#x = y;;
- : bool = false
B.2.5 Blocs et portée des variables
Dans le corps des fonctions, on définit souvent des valeurs locales pour calculer le résultat de la fonction. Ces définitions sont introduites par une construction let ident = expression in .... Il n'y a pas de restriction sur les valeurs locales, et les définitions de fonctions sont admises. Ces fonctions locales sont elles-mêmes susceptibles de comprendre de nouvelles définitions de fonctions si nécessaire et ce ad libitum.
Lorsqu'on cite un identificateur x dans un programme, il fait nécessairement référence au dernier identificateur de nom x lié par une définition let, ou introduit comme paramètre d'une fonction après le mot-clé function. En général, il est plus élégant de garder les variables aussi locales que possible et de minimiser le nombre de variables globales. Ce mode de liaison des identificateurs (qu'on appelle la portée statique) est surprenant dans le système interactif. En effet, on ne peut jamais modifier la définition d'un identificateur; en particulier, la correction d'une fonction incorrecte n'a aucun effet sur les utilisations antérieures de cette fonction dans les fonctions déjà définies.
let successeur x = x - 1;;
let plus_deux x = successeur (successeur x);;
#plus_deux 1;;
- : int = -1
Ici, on constate la bévue dans la définition de successeur, on corrige la fonction, mais cela n'a aucun effet sur la fonction plus_deux. #let successeur x = x + 1;;
successeur : int -> int =
#plus_deux 1;;
- : int = -1
La solution à ce problème est de recharger complètement les fichiers qui définissent le programme. En cas de doute, quitter le système interactif et recommencer la session.
B.2.6 Correction des programmes
Le suivi de l'exécution des fonctions est obtenu à l'aide du mode trace qui permet d'afficher les arguments d'une fonction à l'entrée dans la fonction et le résultat à la sortie. Dans l'exemple du paragraphe précédent, le mécanisme de trace nous renseigne utilement: en traçant la fonction successeur on constate qu'elle n'est jamais appelée pendant l'évaluation de plus_deux 1 (puisque c'est l'ancienne version de successeur qui est appelée). #trace "successeur";;
La fonction successeur est dorénavant tracée.
- : unit = ()
#successeur 1;;
successeur <-- 1
successeur --> 2
- : int = 2
#plus_deux 1;;
- : int = -1
Le mode trace est utile pour suivre le déroulement des calculs, mais moins intéressant pour pister l'évolution de la mémoire. En ce cas, on imprime des messages pendant le déroulement du programme.