Conditions

Syntaxe if

Les conditions permettent de prendre des décisions au cours de l’exécution d’un programme, comme « si la température est supérieure à 30°C, affiche qu’il fait beau ». La syntaxe dédiée est :

(if condition expression-si-oui expression-si-non)

L’ensemble de l’expression if prend la valeur de expression-si-oui si la condition est vraie, et sinon, la valeur de expression-si-non. Prenons un premier exemple :

(define (égaux-ou-différents x y)
  (display
    (if (= x y)
        "x et y sont égaux."
        "x et y sont différents."))
  (newline))

(égaux-ou-différents 4 4)
(égaux-ou-différents 4 5)

On l’aura compris, la procédure = teste si deux nombres sont égaux. Quelle est sa valeur de retour ?

guile> (= 4 5)
#f

Le test s’est évalué à #f, qui est la notation du booléen « faux » (false). L’opposé de #f est #t (« vrai », true).

guile> (= 5 5)
#t

Il est important de remarquer que seule l’une des deux expressions est évaluée, en fonction de la valeur du test. L’exemple précédent pourrait tout aussi bien s’écrire :

(if (= x y)
    (display "x et y sont égaux")
    (display "x et y sont différents"))

Si \(x \neq y\), l’expression (display "x et y sont égaux) n’est jamais évaluée, ce qui est heureux, car autrement, on verrait les deux messages s’afficher à l’écran. Un autre intérêt est de permettre des programmes comme :

(define (affiche-inverse x)
  (if (= x 0)
      (display "impossible de diviser par zéro")
      (display (/ 1 x)))
  (newline))

(affiche-inverse 0)

On affiche ici l’inverse de \(x\) (c’est à dire \(\frac1x\)). Cet inverse n’existe que si \(x \neq 0\), car on ne peut pas diviser par 0. Ce n’est que dans le cas où l’on a bien vérifié que \(x \neq 0\) que l’expression (/ 1 0) s’évalue, ce qui garantit qu’elle ne produira pas d’erreur.

Syntaxe cond

cond est l’équivalent de ce qui dans d’autres langages s’écrit if ... else if ... else if ..., c’est à dire « si… sinon, si…, sinon, si… ». Cette syntaxe est un moyen pratique d’écrire du code qui doit distinguer entre de nombreux cas. Prenons l’exemple d’une tête de note que LilyPond doit afficher en fonction de son style. On pourrait écrire un code dans le goût de :

(if (le-style-est-default)
    (afficher-la-tête-default)
    ; Sinon...
    (if (le-style-est-altdefault)
        (afficher-la-tête-altdefault)
        ; Sinon...
        (if (le-style-est-baroque)
            (afficher-la-tête-baroque)
            ; Sinon...
            (if (le-style-est-mensural)
                (afficher-la-tête-mensural)
                ; Sinon...
                (if (le-style-est-petrucci)
                    (afficher-la-tête-petrucci)
                    ; Sinon...
                    etc. etc. etc.)))))

Bien sûr, ceci est très lourd et les parenthèses pullullent. La syntaxe cond vient à notre secours. Elle se présente comme ceci :

(cond
  (condition1 expression1)
  (condition2 expression2)
  (condition3 expression3)
  ...)

Les conditions sont évaluées dans l’ordre. Dès qu’une condition s’évalue à #t, l’expression correspondante est évaluée et l’expression cond entière prend sa valeur.

À la place de la dernière condition, on peut écrire else, ce qui revient au même que #t. La dernière expression est alors toujours évaluée si aucune des conditions n’est vraie.

Un exemple avec cond :

(define (catégorie température)
  (cond
    ((> température 50)
     "Température invivable !")
    ((> température 40)
     "Température difficilement supportable.")
    ((> température 30)
     "Température chaude à très chaude.")
    ((> température 20)
     "Température modérée.")
    ((> température 10)
     "Température plutôt froide.")
    ((> température 0)
     "Température froide.")
    ((> température -10)
     "Température très froide.")
    (else
     "Température polaire."))
  (newline))

(display (catégorie 28))

Tests courants

Le premier test que nous avons rencontré était =. Il s’applique exclusivement à des nombres. Il teste leur égalité numérique, c’est à dire qu’il ne prend pas en compte le type de nombre, entier ou à virgule.

guile> (= 3 3.0)
#t

Un autre test d’égalité courant est equal?. Il s’applique à tous les types, et non pas seulement aux nombres. On peut par exemple s’en servir pour comparer des chaînes de caractères.

guile> (equal? "Bonjour" "Bonjour")
#t

Même sur des nombres, = et equal? ne reviennent pas au même. En effet, equal? ne considère pas que des nombres de types différents soient égaux.

guile> (equal? 3 3.0)
#f

Le point d’interrogation fait partie du nom de equal? et ne pose pas de problème syntaxique (relire Définition de variables). Il est souvent mis à la fin des noms de procédures qui font un test, c’est à dire qu’elles renvoient vrai ou faux.

Pour comparer des nombres, on dispose des procédures <, <=, > et >=, qui se lisent « strictement inférieur », « inférieur ou égal », « strictement supérieur » et « supérieur ou égal ». Ce sont les équivalents des symboles mathématiques \(<\), \(\leq\), \(>\) et \(\geq\).

guile> (>= 4 3)
#t
guile> (>= 3 4)
#f
guile> (> 3 3)
#f
guile> (>= 3 3)
#t

Combiner des tests

Deux opérateurs logiques principaux permettent de combiner les tests ensemble :

  • and, le ET logique, qui vérifie si toutes les conditions sont vraies ;

  • or, le OU logique, qui teste si au moins l’une des conditions est vraie.

(define (GPS lieu)
  (if (or (equal? lieu "Pôle Nord")
          (equal? lieu "Pôle Sud"))
      (display "Enfilez les manteaux !")
      (display "Pas encore arrivés au pôle."))
  (newline))

(GPS "Pôle Nord")
(GPS "Helsinki")
(define (détecte-requiem compositeur œuvre)
  (cond
    ((and (equal? œuvre "Requiem")
          (equal? compositeur "Mozart"))
     (display "Le requiem de Mozart"))
    ((equal? œuvre "Requiem")
     (display "Un certain requiem (pas de Mozart)"))
    (else
     (display "Pas un requiem")))
  (newline))

(détecte-requiem "Mozart" "Requiem")

L’opérateur not est un NON logique, qui inverse un test.

(define (conversation est-musicien logiciel-préféré)
  (cond
    ((not est-musicien)
     (display "Quel dommage pour vous !"))
    ((not (equal? logiciel-préféré "LilyPond"))
     (display "Connaissez-vous un logiciel formidable appelé LilyPond ?"))
    (else
     (display "À quand votre première contribution ?")))
  (newline))

(conversation #t "LilyPond")

« if manchot »

Il arrive assez souvent qu’il n’y ait rien à faire si une condition est fausse. Ceci est particulièrement vrai pour les messages d’erreur. Il existe pour ces cas une deuxième forme de l’instruction if, où l’on omet tout simplement l’expression à évaluer quand la condition est fausse :

(if condition expression-si-oui)

Par exemple :

(define (vérifie-compositeur nom)
  (if (equal? nom "inconnu")
      (error "compositeur non trouvé dans la base de données")))

(vérifie-compositeur "inconnu")

Si la condition est vraie, expression-si-oui est évaluée et le if entier prend cette valeur, comme dans les if habituels. Si la condition est fausse, l’expression n’a aucune valeur intéressante.

guile> (if #t "valeur si oui")
"valeur si oui"
guile> (if #f "valeur si oui")
guile>

Mais elle a tout de même une valeur !

guile> *unspecified*
guile> (equal? *unspecified* (if #f "valeur si oui"))
#t

Cette valeur est *unspecified*, une constante spéciale renvoyée par toutes les fonctions qui n’ont besoin de renvoyer aucune valeur particulière. La fonction display renvoie également *unspecified*. Pour ne pas alourdir la sortie, le bac à sable ignore tout simplement *unspecified*.

De la vérité universelle

Les if n’acceptent pas seulement des booléens comme conditions. Dans d’autres langages, certains types possèdent des règles spécifiques quant à la valeur de vérité. Par exemple, les nombres sont généralement considérés comme vrais, sauf 0. De même, les chaînes de caractères sont vraies, sauf les chaînes vides, et les listes sont vraies, sauf les listes vides.

En Scheme, les choses sont un peu différentes. Absolument toutes les valeurs sont vraies, sauf le booléen #f. En particulier, 0 et les chaînes vides sont vrais.

guile> (if 0 "oui" "non")
"oui"

C’est pourquoi on prend généralement #f pour représenter les valeurs manquantes, non trouvées, etc. Avec cette convention, la fonction vérifie-compositeur se réécrit :

(define (vérifie-compositeur nom) ; nom est une chaîne de caractères ou #f
  (if (not nom) ; détecte #f
      (error "compositeur non trouvé dans la base de données")))

De même que if, and et or n’évaluent leurs arguments que lorsque cela s’avère nécessaire. De plus, ils ne renvoient pas forcément un booléen. and renvoie la valeur de la dernière expression, tandis que or renvoie la valeur de la première expression vraie. Cette propriété est à la base d’une astuce fréquente qui consiste à abuser de or pour définir des valeurs par défaut.

(define (affiche-compositeur compositeur)
  (display
    (or compositeur "[inconnu]"))
  (newline))

(affiche-compositeur "Mozart")
(affiche-compositeur #f)

Décortiquons ce qui se produit. Si compositeur est une valeur vraie (n’importe quel objet sauf #f), le test or compositeur peut s’arrêter, puisque or a déjà trouvé une valeur vraie (il en faut au moins une). or renvoie alors cette valeur, qui est compositeur. Si compositeur vaut #f, alors le test or ne peut pas s’arrêter là et évalue l’expression suivante, qui est "[inconnu]". Cette expression est vraie, donc il termine et la renvoie.