Introduction▲
La grammaire de F# est relativement simple et consistante. Il n'y a que deux types d'éléments : les expressions et les déclarations. Quand une expression est évaluée, elle se simplifie en une valeur simple. Toutes les valeurs ont un type (et un seul).
Si vous avez accès au mode interactif de F# (fsi.exe), je vous conseille de l'essayer. Dans ce cours et les suivants, beaucoup d'exemples sont donnés. Je vous recommande de tester ces exemples par vous-même pour mieux comprendre. Dans le mode interactif, l'invite de commande est représentée par un chevron <. Il faut taper ;; suivi d'entrée pour envoyer la commande à l'interpréteur. L'interpréteur évalue ensuite la commande. Si la commande est une expression, elle est simplifiée. La valeur de retour est affichée, ainsi que son type.
Pour se familiariser avec la syntaxe, voici des exemples d'expressions. En général, ça devrait assez simple à comprendre. Les commentaires en F# sont :
- // commente jusqu'à la fin de la ligne
- (* commente jusqu'au *) associé (ça peut être imbriqué)
Dans les exemples qui suivent, j'ai utilisé des valeurs de base et quelques opérateurs. On verra les fonctions dans un prochain cours.
I. Les Types de base▲
I-1. Les Entiers▲
>
42
;;
val
it : int
=
42
Le type de l'expression "42" est donc int (entier). Sa valeur est "42".
>
5
+
5
;;
val
it : int
=
10
Le type de l'expression "5 + 5" est donc int. Sa valeur est "10".
>
4
*
(5
-
3
);;
val
it : int
=
8
>
(5
+
6
) % 10
;; //
% correspond au modulo.
val
it : int
=
1
I-2. Les Flottants▲
>
4
.;;
val
it : float
=
4
.0
>
4
.0
;;
val
it : float
=
4
.0
>
4
.5
;;
val
it : float
=
4
.5
>
4
.2
+
5
.3
*
4
.1
;; //
+
fonctionne aussi sur les flottants
val
it : float
=
25
.93
>
2
. **
8
.;; //
**
est l'opérateur puissance.
val
it : float
=
256
.0
I-3. Les Caractères▲
>
'a';;
val
it : char
=
'a'
>
'\n';; //
saut de ligne
val
it : char
=
'\n'
>
'\'';;
val
it : char
=
'\''
>
''';; //
raccourci syntaxique
val
it : char
=
'\''
I-4. Les Booléens▲
>
true
;;
val
it : bool
=
true
>
false
;;
val
it : bool
=
false
>
4
=
5
;; //
en C/
C++
: ==
val
it : bool
=
false
>
4
<
42
;;
val
it : bool
=
true
>
4
<>
42
;; //
en C/
C++
: !=
val
it : bool
=
true
>
'a' <
'b';;
val
it : bool
=
true
>
true
|| false
;;
val
it : bool
=
true
>
3
<
4
&& 'c' >
'a';; //
&& a une faible priorité.
val
it : bool
=
true
I-5. Les Chaînes de caractères▲
>
"test"
;;
val
it : string
=
"test"
>
"Hello "
+
"world!"
;; //
concaténation
val
it : string
=
"Hello world!"
>
"test"
.[0
];; //
Accès à un caractère (ça commence à 0
)
val
it : char
=
't'
>
"test"
.[1
];;
val
it : char
=
'e'
>
"c:\\games\\"
;;
val
it : string
=
"c:\\games\\"
>
@
"c:\game\";; // chaine "
verbatim" : les \ ne sont pas interprétés
val
it : string
=
"c:\\games\\"
>
"test"
.[0
..2
];; //
sous-
chaine
val
it : string
=
"tes"
>
"test"
.[2
..3
];;
val
it : string
=
"st"
>
"test"
.[2
..2
];;
val
it : string
=
"s"
I-6. Les Listes▲
Les listes sont toujours paramétrées par un type : int list correspond au type liste d'entiers. Il peut aussi être noté list<int>. Niveau implémentation, le type liste correspond à une liste chainée.
>
[1
; 4
; 6
; 10
; 5
];; //
liste litérale
val
it : int
list
=
[1
; 4
; 6
; 10
; 5
]
>
["this"
; "is"
; "a"
; "test"
];;
val
it : string
list
=
["this"
; "is"
; "a"
; "test"
]
>
2
::
[3
; 4
];; //
::
est l'opérateur de construction de listes
val
it : int
list
=
[2
; 3
; 4
]
>
1
::
4
::
[5
];; //
l'opérateur ::
est associatif à droite
val
it : int
list
=
[1
; 4
; 5
]
>
1
::
4
::
5
::
[];;
val
it : int
list
=
[1
; 4
; 5
]
>
[4
; 5
] @
[10
; 4
];; //
@
est l'opérateur de concaténation de listes
val
it : int
list
=
[4
; 5
; 10
; 4
]
>
[1
..10
];; //
range comprehension
val
it : int
list
=
[1
; 2
; 3
; 4
; 5
; 6
; 7
; 8
; 9
; 10
]
>
[3
..2
..10
];; //
pareil, avec incrément
val
it : int
list
=
[3
; 5
; 7
; 9
]
>
[10
.. -
1
.. 5
];;
val
it : int
list
=
[10
; 9
; 8
; 7
; 6
; 5
]
I-7. Les Tableaux▲
De la même façon que les listes, ils sont paramétrés par un type. Les éléments d'un tableau sont stockés ensemble dans la mémoire, ce qui permet un accès direct.
>
[| 3
; 6
; 10
; 5
|];;
val
it : int
array
=
[|3
; 6
; 10
; 5
|]
>
[| 4
; 10
; 5
|].[0
];;
val
it : int
=
4
>
[| 4
; 10
; 5
|].[1
];;
val
it : int
=
10
>
[| 4
; 10
; 5
|].[0
..1
];; //
sous-
tableau
val
it : int
array
=
[|4
; 10
|]
>
[| 'a' .. 'e' |];;
val
it : char
array
=
[|'a'; 'b'; 'c'; 'd'; 'e'|]
>
[| 8
.. 12
|].[2
];;
val
it : int
=
10
>
[| 80
.. 3
.. 100
|].[2
..5
];;
val
it : int
array
=
[|86
; 89
; 92
; 95
|]
I-8. Les Tuples▲
Un tuple permet de regrouper plusieurs valeurs. Ça peut être vu comme une structure anonyme, dont les champs sont anonymes. Voici quelques exemples, regardez bien le type des valeurs :
>
2
, 3
;;
val
it : int
*
int
=
(2
, 3
)
>
"test"
, 4
;;
val
it : string
*
int
=
("test"
, 4
)
>
2
, 4
, 10
;;
val
it : int
*
int
*
int
=
(2
, 4
, 10
)
>
[2
; 3
], 4
, "test"
;;
val
it : int
list
*
int
*
string
=
([2
; 3
], 4
, "test"
)
Pour ce dernier exemple, le type indique que c'est un tuple composé d'une liste d'entiers, d'un entier et d'une chaine de caractères.
>
[1
, "this"
; 2
, "is"
; 3
, "a"
; 4
, "test"
];;
val
it : (int
*
string
) list
=
[(1
, "this"
); (2
, "is"
); (3
, "a"
); (4
, "test"
)]
Ceci est une liste de couples.
II. Les Expressions▲
II-1. Du typage fort▲
Les expressions suivantes sont mal typées et génèrent une erreur dès la compilation.
4
+
4
.2
//
on n'ajoute pas deux valeurs de types différents
'a' +
1
//
même remarque. Et il n'y a jamais de cast implicite
[2
; 4
; "test"
] //
les éléments d'une liste doivent tous avoir le même type
[| 4
; true
|] //
pareil pour les tableaux
4
=
4
. //
on ne compare
pas deux types différents
(4
, 3
) =
(4
, 5
, 2
) //
les tuples n'ont pas le même type
.
(3
, 'a') =
('a', 3
) //
même problème : l'ordre dans un tuple est important.
Pour convertir un entier en flottant, on utilise la fonction float. Pour la conversion inverse, c'est int (tronque la partie décimale).
II-2. Les Expressions conditionnelles▲
Les expressions conditionnelles ont la syntaxe suivante :
if
<
expr1>
then
<
expr2>
else
<
expr3>
expr1, expr2 et expr3 sont des expressions quelconques. Il faut toutefois que expr1 s'évalue en type bool,
et que expr2 et expr3 aient le même type t. Cette expression renverra soit l'évaluation de expr2 (si expr1 vaut true),
soit l'évaluation de expr3 (si expr1 vaut false).
Ainsi, le if then else est l'équivalent de l'opérateur ternaire du C. Il renvoie toujours une valeur.
Et il n'y a pas d'équivalent au if du C.
>
if
true
then
4
else
5
;;
val
it : int
=
4
>
if
4
>
5
then
"test"
else
"foo"
;;
val
it : string
=
"foo"
>
"abcdefgh"
.[if
3
*
3
<
5
then
0
else
2
];;
val
it : char
=
'c'
Partout où l'on peut mettre une expression, on peut mettre une condition.
Les deux expressions suivantes sont mal typées :
if
4
then
1
else
2
//
4
n'est pas un booléen
if
true
then
4
.5
else
2
//
4
.5
et 2
n'ont pas le même type
III. Les Déclarations▲
III-1. Les Déclarations locales▲
Pour associer un nom à une valeur, on utilise la construction let..in. Elle a cette syntaxe :
let
<
ident>
=
<
expr1>
in
<
expr2>
expr1 et expr2 sont deux expressions quelconques, de n'importe quels types. Pour l'identifiant, il faut suivre (en gros),
cette expression rationnelle : [a-zA-Z_][a-zA-Z_0-9']*
Ainsi, les identifiants suivants sont valides :
- toto
- Test
- g'
- _foo_bar42
Dans expr2, on peut utiliser l'identifiant. Il aura la même valeur que exp1. Voici quelques exemples de définitions locales :
>
let
x =
6
in
6
*
6
;;
val
it : int
=
36
>
let
x =
"a"
in
x +
x +
x;;
val
it : string
=
"aaa"
Le bloc "let in" étant lui-même une expression, il est possible de les imbriquer.
>
let
x =
5
in
let
y =
x +
1
in
x +
y;;
val
it : int
=
11
Partout, absolument partout, où l'on attend une expression, il est possible de définir localement une valeur. Par exemple :
>
[| 1
.. let
x =
3
in
x *
x |];;
val
it : int
array
=
[|1
; 2
; 3
; 4
; 5
; 6
; 7
; 8
; 9
|]
>
"test"
.[let
x =
3
in
x -
2
];;
val
it : char
=
'e'
Avec la construction let in, on peut masquer une définition déjà existante. Ainsi, si on écrit let x = x + 1, le x de l'expression fait référence à un x déjà défini auparavant. Par exemple :
>
let
x =
2
in
let
x =
x +
1
in
x;;
val
it : int
=
3
Bien sûr, ce genre de construction est à éviter, mais c'est pour montrer comment est gérée la portée.
III-2. Les Déclarations globales▲
On souhaite parfois définir une valeur de façon globale. On utilise pour cela la construction
let <ident> = <expression>.
L'identifiant est alors visible dans la suite du programme.
Ainsi :
>
let
x =
4
;;
val
x : int
>
x;;
val
it : int
=
4
>
x +
1
;;
val
it : int
=
5
Il est important de noter qu'une définition globale n'est pas une expression. On ne peut donc pas l'utiliser là où une expression est attendue.
>
let
a =
let
x =
5
in
let
y =
6
in
x *
y;;
val
a : int
>
a;;
val
it : int
=
30
IV. Mode #light▲
Pour activer le mode light, il suffit de taper #light dans le mode interactif ou de mettre cette commande
au début du fichier. Quand il est activé, la grammaire est légèrement modifiée : elle est allégée et se base
sur l'indentation pour désambiguiser la syntaxe. Cela impose une certaine rigueur
(même si les règles d'indentation sont relativement souples), mais permet d'avoir du code bien plus court et lisible.
Ce mode light n'interdit pas d'utiliser le "in" si on le souhaite (ce qui est
pratique lorsque l'on veut mettre une expression sur une ligne), ni l'utilisation
de blocs explicites.
Les spécifications formelles de la syntaxe sont un peu complexes, mais elles sont plutôt intuitives. Par la suite,
j'utiliserai toujours (sauf mention contraire) cette syntaxe light. Et je vous conseille
fortement de l'utiliser systématiquement, d'autant plus qu'il sera mis par défaut
dans une prochaine version.
Le dernier exemple donné devient :
>
let
a =
let
x =
5
let
y =
6
x *
y;;
val
a : int
Comme vous pouvez le voir, tous les "in" peuvent être omis. Nous verrons les autres différences plus tard.
Vous pouvez vous rendre compte que le code n'est pas ambigü : la valeur de a étant forcément une expression,
les définitions de x et de y ne peuvent être que locales.
D'une manière générale, deux valeurs qui ont la même portée ont la même indentation. Pour l'indentation,
vous pouvez utiliser le nombre d'espaces que vous souhaitez, mais soyez consistants. Et surtout, n'utilisez jamais de tabulation
(configurez votre éditeur si nécessaire). Voici un exemple complet, qui compile, et qui reprend la plupart des choses vues
jusqu'à maintenant (regardez bien l'indentation) :
#light
let
a =
let
x =
10
let
y =
15
x *
y
let
b =
a % 5
let
c =
if
b <
3
then
"oui"
else
"non"
let
d =
c.[0
..1
]
printfn "a = %d, b = %d, c = %s, d = %s"
a b c d
$ fsc test.fs
$ ./test.exe
a = 150, b = 0, c = oui, d = ou
$