F# : Types de base, expressions et déclarations
Date de publication : 14 juin 2008
Par
Laurent Le Brun (Tutoriels)
Dans cet article, vous retrouverez les fondements de F# : types de bases, expressions, déclarations de valeurs. Les fonctions seront abordées dans un article ultérieur.
Attention, n'oubliez surtout pas de consulter la dernière partie consacrée au mode #light de F# qui permet d'obtenir une syntaxe encore plus concise de son code, au prix d'une indentation rigoureuse.
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
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 :
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
$
|


Copyright (c) 2008 Laurent Le Brun. Permission is granted to copy and distribute under the terms of the Creative Commons
licence, Version 3.0 or any later version published by the Creative Commons Corporation; with Attribution, No Commercial Use and
No Derivs. Read the full license here : http://creativecommons.org/licenses/by-nc-nd/3.0/legalcode