La règle que l'on va annoncer peut sembler déroutante au début, mais on se rendra vite compte qu'en fait on l'utilise déjà sans vraiment le savoir.
Une expression "expr1; expr2"
, où exrp1
est une expression de type unit, est de la valeur et du type de l'expression expr2
.
Tout de suite un exemple. On a vu juste avant que print_string "test"
est une expression de type unit
, et que 5 était une expression de type int
. Donc d'après la règle, la juxtaposition des deux :
print_string "test"; 5
forme une expression de type int
, qui vaut 5. Puisque cette expression représente l'entier 5, on va l'afficher avec un print_int
, en prenant soin de l'entourer de parenthèses :
print_int (print_string "test"; 5)
Cette ligne forme donc une expression de type unit
: c'est notre programme dans son ensemble.
Regardons maintenant ce qu'il se passe lorsqu'on exécute ce programme.
Pour évaluer l'expression expr1; expr2
où exrp1
est une expression de type unit, on commence par faire l'action de expr1
, puis on calcul la valeur de expr2
.
Donc pour évaluer print_string "test"; 5
, on commence par afficher "test"
, et ensuite on donne la valeur 5. Ainsi le code :
print_int (print_string "test"; 5)affiche
test5
.
Remarque : on peut aussi utiliser une déclaration pour nommer l'expression print_string "test"; 5
. Ce code est équivalent au précédent :
let x = print_string "test"; 5 in print_int x
On précisera le fonctionnement du let
dans la partie suivante.
Revenons à la règle. Dans expr1; expr2
, lorsque expr1
et expr2
sont tous les deux de type unit
, puisque l'ensemble forme une expression du même type que expr2
, il est donc de type unit
.
Le code suivant est la juxtaposition de deux expressions de type unit
, il fait quelque chose :
print_string "test"; print_newline()
Voyons maintenant ce qu'il se passe-t-il si l'on juxtapose 3 expressions de type unit
.
print_string "test"; print_int 5; print_string "cinq"
C'est très simple, le compilateur regroupe les deux premières expressions :
( print_string "test"; print_int 5 ) ; print_string "cinq"
Il peut maintenant décortiquer correctement le code à l'aide de la règle énoncé en début de partie. D'abord :
print_string "test"; print_int 5
est la juxtaposition de deux expressions de type unit
, donc une expression de type unit
.
Ensuite, on va juxtaposer cette expression (qui est déjà elle-même une juxtaposition) avec l'expression print_string "cinq"
qui est aussi de type unit
. Au final,
print_string "test"; print_int 5 ; print_string "cinq"
est une expression de type unit
. C'est un code valide, qui décrit un programme
test5cinq
.
Dernière chose à voir : une suite de plusieurs expressions de type unit
, terminé par une expression d'un autre type que unit
. Exemple :
print_string "test"; print_newline(); 5
est une expression de type int
, qui vaut 5.
Pour aboutir à ce résultat, le compilateur à simplement lu :
( print_string "test"; print_newline() ) ; 5D'abord
print_string "test"; print_newline()
est la juxtaposition de deux expressions de type unit
, donc est de type unit
.
Ensuite
( print_string "test"; print_newline() ) ; 5
est la juxtaposition d'une expression de type unit
et d'une autre de type int
, et est donc au final une expression de type int
.
On peut affiche cette expression avec un print_int
:
print_int ( print_string "test"; print_newline(); 5 )
Ce code affiche "test"
, puis un retour à la ligne, puis 5 :
test 5
On peut généraliser ces deux constructions en disant que :
expr1; expr2; ... exprN-1; exprN
où tous les termes de expr1
à exprN-1
sont forcément de type unit
constitue une expression dont la valeur et le type est celui de exprN
.
Si une des expressions entre expr1
et exprN-1
n'est pas de type unit
, on obtient une erreur. Un exemple :
print_string "test"; 5 ; print_string "cinq"
File "test.ml", line 1, characters 21-22: Warning: this expression should have type unit.
Le problème est localisé sur le 5
qui devrait être de type unit
, mais qui ne l'est pas, puisque 5 est un entier.
Il ne s'agit pas d'une erreur, mais seulement d'un avertissement (warning
). Le code fonctionne donc, la valeur 5 est simplement ignorée, et le programme affiche : testcinq
.
On peut forcer un certain groupement d'expressions avec des parenthèses ouvrante puis fermante, ou bien avec les bornes begin
et end
.
Ainsi begin expr1 end
comme (expr1)
sont simplement équivalents à expr1
.
On a déjà vu un exemple :
print_int (print_string "test"; 5)
Voici le même avec begin
et end
:
print_int begin print_string "test"; 5 end
On reviendra sur les blocs au moment des problèmes de surdéfinition, et aussi pour les structures if
.
déroutante au début.
L'expression let name = expr1 in expr2
vaut expr2
dans laquelle name
est un nom qui peut être utilisé dans expr2
, et qui représente la valeur de expr1
qu'on calcul une fois pour toutes.
Remarque : le nom name
est associée à une valeur uniquement dans expr2
. Mais ce nom n'est associé à rien dans expr1
ou ailleurs (à moins qu'il y ait ailleurs une autre définition associant name
à une valeur).
Reprenons un de nos tout premiers programmes, et décortiquons-le comme le fait le compilateur :
let x = 238 in print_int x; print_string " "; print_int (x * x)
Explications. Le nom associé à une valeur est x
. La valeur associé à ce nom est 238
. L'expression dans laquelle le nom x
est associé à la valeur 238
est l'expression :
print_int x; print_string " "; print_int (x * x)
C'est une juxtaposition de trois expressions de type unit
, donc une expression de type unit
. L'ensemble du programme est donc aussi une expression de type unit
.
Remarque : le programme peut être terminé par un point-virgule si on veut, mais il n'y en a pas besoin puisqu'il n'y a rien après.
Décortiquons maintenant un autre programme, avec deux let
:
let x = read_int() in let y = read_int() in print_int (x + y)
Il y a deux déclarations. Dans la première, on associe le nom x
à une première valeur donnée par l'utilisateur, et ce à l'intérieur de l'expression :
let y = read_int() in print_int (x + y)
Dans cette seconde déclaration, on associe le nom y
à une seconde valeur donnée par l'utilisateur, et ce à l'intérieur de l'expression :
print_int (x + y)
Cette dernière expression est de type unit
. Donc
let y = read_int() in print_int (x + y)
est aussi de type unit
. Et par conséquent :
let x = read_int() in let y = read_int() in print_int (x + y)
est une expression de type unit
, qui forme la totalité du programme.
Regardons alors ce qui se passe lors de l'évaluation. D'abord on donne une valeur à x
, par exemple 5. Ainsi le code est équivalent à :
let y = read_int() in print_int (5 + y)
Ensuite on donne une valeur à y
, par exemple 8. Il reste :
print_int (5 + 8)
Ce code affiche 13, qui est le résultat attendu du programme.
Dans certains cas, il peut y avoir une ambiguïté sur la manière de lire les expressions. Il y a ainsi deux façons d lire :
let x = read_int() in print_int x; let y = read_int() in print_int y;
La première méthode, comme déclaration dans un bloc :
let x = read_int() in begin print_int x; let y = read_int() in print_int y end
La seconde méthode, comme juxtaposition de deux blocs contenant chacun une déclaration :
begin let x = read_int() in print_int x end; begin let y = read_int() in print_int y end
Dans une telle situation, le compilateur a une méthode pour choisir un des deux découpages possibles. Mais comme on n'a pas besoin de connaître cette méthode en détails, on n'en dira pas plus. De toutes façons, le résultat dans les deux cas est équivalent.