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

 
Sélectionnez

> 42;;
val it : int = 42

Le type de l'expression "42" est donc int (entier). Sa valeur est "42".

 
Sélectionnez

> 5 + 5;;
val it : int = 10

Le type de l'expression "5 + 5" est donc int. Sa valeur est "10".

 
Sélectionnez

> 4 * (5 - 3);;
val it : int = 8
> (5 + 6) % 10;;        // % correspond au modulo.
val it : int = 1

I-2. Les Flottants

 
Sélectionnez

> 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

 
Sélectionnez

> '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

 
Sélectionnez

> 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

 
Sélectionnez

> "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.

 
Sélectionnez

> [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.

 
Sélectionnez

> [| 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 :

 
Sélectionnez

> 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.

 
Sélectionnez

> [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.

 
Sélectionnez

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 :

 
Sélectionnez

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.

 
Sélectionnez

> 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 :

 
Sélectionnez

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 :

 
Sélectionnez

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 :

 
Sélectionnez

> 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.

 
Sélectionnez

> 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 :

 
Sélectionnez

> [| 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 :

 
Sélectionnez

> 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 :

 
Sélectionnez

> 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.

 
Sélectionnez

> 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 :

 
Sélectionnez

> 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) :

 
Sélectionnez

#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
 
Sélectionnez

$ fsc test.fs
$ ./test.exe
a = 150, b = 0, c = oui, d = ou
$