L01 - Types de base et expressions

Ce billet présente les types les plus courants de F# : entiers, chaines de caractères, listes, etc.

La grammaire de F# est relativement simple et cohérente. 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 le 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.

Les entiers

[fsharp]
> 42;;
val it : int = 42

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

[fsharp]
> 5 + 5;;
val it : int = 10

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

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

Les flottants

[fsharp]
> 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

Les caractères

[fsharp]
> 'a';;
val it : char = 'a'
> '\n';;                // saut de ligne
val it : char = '\n'
> '\'';;
val it : char = '\''
> ''';;                 // raccourci syntaxique
val it : char = '\''

Les booléens

[fsharp]
> 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

Les chaines de caractères

[fsharp]
> "test";;
val it : string = "test"
> "Hello " + "world!";;           // concaténation
val it : string = "Hello world!"
> "test".[0];;                    // Accès à un caractère (0 est le premier caractère)
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"

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 simplement chainée. Il est très rapide d'ajouter un élément au début, mais il est coûteux d'en ajouter un à la fin.

[fsharp]
> [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];;          // en utilisant un incrément
val it : int list = [3; 5; 7; 9]
> [10 .. -1 .. 5];;
val it : int list = [10; 9; 8; 7; 6; 5]

Les tableaux

De la même façon que les listes, les tableaux 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.

[fsharp]
> [| 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 .. 1 .. 100 |].[2..5];;
val it : int array = [|82; 83; 84; 85|]

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 :

[fsharp]
> 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 cractères.

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

Certaines personnes préfèrent entourer les tuples de parenthèses pour des raisons de lisibilité, mais ce n'est pas obligatoire.

Du typage fort

Les expressions suivantes sont mal typées et génèrent une erreur dès la compilation.

[fsharp]
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 conversion 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. est un nombre flottant

(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). L'opérateur = teste l'égalité entre deux valeurs en profondeur. Il est donc tout à fait adapté pour comparer des chaînes de caractères, des listes, des tableaux...

Comments

1. On Wednesday, May 21 2008, 12:58 by Diana

J'utilise la version 1.9.4.17.

1) Pour le ligne de code :
> 3 < 4 && 'c' > 'a';; // && a une faible priorité.
le resultat e "true"
Vous avez ecrit : val it : bool = false

2) La construction
> [10 .. -1 .. 5];;
n'est plus supporte. "This construct is deprecated"

2. On Wednesday, May 21 2008, 17:59 by Laurent

Le premier point vient d'être corrigé, merci.

Pour le 2e, je ne suis pas d'accord. J'ai essayé plusieurs version et je n'ai pas d'avertissement. Cela dit, un autre exemple avec des flottants génère maintenant un avertissement. Je viens de le modifier.

3. On Thursday, July 24 2008, 09:02 by Volia

Il y a un truc que je ne comprend pas :

F# Version 1.9.4.19, compiling for .NET Framework Version v2.0.50727

> [1..1..10];;
val it : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
> [1 .. 1 .. 10];;
val it : int list = [1; 2; 3; 4; 5; 6; 7; 8; 9; 10]
> [10 .. -1 .. 1];;
val it : int list = [10; 9; 8; 7; 6; 5; 4; 3; 2; 1]
> [10..-1..1];;

[10..-1..1] ;;
-----^^^^^^

stdin(28,5): warning FS0042: This construct is deprecated: it is only for use in
the F# library

[10..-1..1] ;;
-----^^^^^^

stdin(28,5): error FS0191: array access or constructor field access expected.
>
[10..-1..1] ;;
----------^^

stdin(28,10): error FS0191: syntax error.

Donc avec les espaces autour de l'opérateur ".." ça marche toujours, mais si on les omets ça ne marche que pour les nombres positifs.
Je suppute un problème sur la lecture de "..-", mais est-ce un comportement normal ?

4. On Friday, July 25 2008, 01:07 by Laurent

Oui, c'est normal. F# permet de définir ses propres opérateurs. "..-" est un nom d'opérateur valide, tout comme "..". Par exemple :

> let (..-) = (+);;
val ( ..- ) : (int -> int -> int)
> 1 ..- 2;;
val it : int = 3

5. On Tuesday, September 1 2009, 20:42 by Laurent

Il est intéressant de noter que le code [10..-1..1] est maintenant accepté par le compilateur (testé avec la version 1.9.6.16). L'espace avec le moins n'est plus requise.

Comme il est toujours possible de définir l'opérateur (..-), il existe une différence entre "10..-5" et "10 ..- 5" (seul le premier sert comme intervalle). Ce nouveau comportement me semble plus intuitif.