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 (en F#, il n'y a pas la notion d'instruction, il n'y a que des expressions - on reviendra sur ce point plus tard).

> 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 

Déclaration locale

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. Pour mieux comprendre, voici quelques exemples de définitions locales :

> let x = 6 in x * x;;
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. Donc si l'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 à manipuler avec précaution, mais c'est pour montrer comment est gérée la portée.

Définition globale

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.

> 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

Syntaxe basée sur l'indentation

Par défaut (depuis mai 2009), F# utilise l'indentation du code pour le comprendre. Quand ce mode, historiquement "light", est activé, on peut se passer de certains mots-clés. Cela permet d'avoir du code bien plus court et léger.

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 basée sur l'indentation.

Le dernier exemple donné s'écrit de façon plus simple :

> 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. Vous pouvez utiliser le nombre d'espaces que vous souhaitez, mais soyez cohérents. N'utilisez jamais de tabulation (configurez votre éditeur si nécessaire), mais rassurez-vous : le compilateur vous rappellera à l'ordre si vous oubliez cette règle. Voici un exemple complet, qui compile, et qui reprend la plupart des choses vues jusqu'à maintenant (regardez bien l'indentation) :

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

Ce code affiche :

a = 150, b = 0, c = oui, d = ou

On peut demander au compilateur de ne pas prendre en compte l'indentation, il suffit d'indiquer au début de ses fichiers :

#indent "off" 

Cela peut être utile si l'on génère du code F#. Je sais que certains sont hostiles à ce concept de donner un sens aux espaces, mais il allège réellement la syntaxe et permet d'éviter certaines erreurs classiques. La syntaxe est suffisamment stricte pour qu'une faute de frappe génère presque toujours une erreur à la compilation ; elle est en même temps assez souple pour ne pas être trop contraignante. Il existe par exemple de nombreuses façons de formatter un "if then else", le compilateur acceptera toutes celles qui sont cohérentes. Essayez, voyez à quel point le compilateur est tolérant (il l'est beaucoup plus que dans Python, par exemple). Notez aussi qu'il est toujours possible d'utiliser un "in" si on le souhaite (c'est utile si on veut écrire une expression en une ligne).