Control Flow

Structures de contrôle

Blocs

Les blocs regroupent plusieurs instructions et retournent la valeur de la dernière expression.

# Bloc simple
resultat = {
    x = 10
    y = 20
    x + y          # Valeur retournée: 30
}

# Bloc vide
vide = {}          # Retourne None

Un bloc porte un scope : x et y ci-dessus n'existent plus après l'accolade fermante. C'est vrai aussi des corps de for et de while, pas des branches d'un if ni des bras d'un match — voir SCOPES_AND_VARIABLES pour le critère.

Conditions (if/elif/else)

# simple if
if x > 0 {
    print("x is positive")
}

# if/else
if x > 0 {
    print("positive")
} else {
    print("negative or zero")
}

# if/elif/else
if x > 0 {
    print("positive")
} elif x < 0 {
    print("negative")
} else {
    print("zero")
}

# nested conditions
if x > 0 {
    if x > 100 {
        print("large number")
    } else {
        print("small number")
    }
}

elif est du sucre syntaxique pour un else contenant un if.

if comme expression

if est une expression, pas un statement.

result = if x > 0 { "positive" } else { "non-positive" }

Un bloc { … } a pour valeur la valeur de sa dernière expression.

La valeur d'un if est définie par les règles suivantes.

Forme sans else

if cond { A }

est une expression dont la valeur est :

  • si cond est vrai → la valeur de A
  • si cond est faux → None
value = if False { 123 }
# value == None

Forme avec else

if cond { A } else { B }

est une expression dont la valeur est :

  • si cond est vrai → la valeur de A
  • si cond est faux → la valeur de B

Forme avec elif

if cond1 { A } elif cond2 { B } else { C }

est équivalent à :

if cond1 {
    A
} else {
    if cond2 {
        B
    } else {
        C
    }
}

et suit les mêmes règles de valeur.

Mermaid diagram lang__CONTROL_FLOW--m001 Mermaid diagram lang__CONTROL_FLOW--m001

Interaction avec les fonctions

Le corps d'une fonction est un bloc expression :

f = (x) => {
    if x > 0 {
        "positive"
    } else {
        "non-positive"
    }
}

ici, la valeur de la fonction est la valeur de l'expression if.

return expr sort explicitement de la fonction avec la valeur expr.

si aucun return explicite n'est exécuté, la fonction renvoie la valeur de la dernière expression du bloc.

Note pour voyageurs inter-langages

Dans Catnip, if est une expression, et renvoie une vraie valeur.

Et un if sans else renvoie None quand la condition est fausse.

Boucle while

# Boucle while simple
i = 0
while i < 5 {
    print("i =", i)
    i = i + 1
}

# Condition complexe
somme = 0
n = 1
while somme < 100 {
    somme = somme + n
    n = n + 1
}

Boucle for

nombres = list(10, 20, 30)

# Itération sur une séquence
for i in range(1, 6) {
    print("i =", i)
}

# Itération sur une liste
for nom in list("Capitaine Whiskers", "Docteur Latte", "Agent Photon") {
    print("BORN TO SEGFAULT,", nom, "!")
}

# Avec enumerate
for (idx, valeur) in enumerate(nombres) {
    print("Index:", idx, "Valeur:", valeur)
}

Valeur d'une boucle

Une boucle est une expression : elle rend la dernière valeur de son corps, comme n'importe quel bloc. Si le corps ne s'exécute jamais, elle rend None.

for i in list(1, 2, 3) { i * 10 }     # 30
for i in list() { i * 10 }            # None -- le corps n'a jamais tourné

n = 0
while n < 3 { n = n + 1; n * 2 }      # 6

Une itération interrompue ne contribue pas : break et continue sortent avant que la valeur du tour ne soit retenue, donc c'est celle du dernier tour complet qui reste.

for i in list(1, 2, 3) { if i == 2 { break }; i }      # 1 -- le tour 2 n'a rien rendu
for i in list(1, 2, 3) { if i == 1 { break }; i }      # None -- aucun tour complet
for i in list(1, 2, 3) { if i == 3 { continue }; i }   # 2

Une boucle qui n'a jamais tourné et une boucle interrompue au premier tour rendent la même chose. Le langage ne distingue pas « rien fait » de « rien fini ».

Contrôle de flux dans les boucles : break et continue

break - Sortir d'une boucle

Le mot-clé break permet de sortir immédiatement d'une boucle while ou for.

# Recherche dans une liste
found = False
for i in list(1, 5, 10, 15, 20) {
    if i == 10 {
        found = True
        break  # Sort de la boucle dès qu'on trouve
    }
}

# Boucle infinie avec break
count = 0
while True {
    count = count + 1
    if count == 5 {
        break  # Sort après 5 itérations
    }
}

# Dans des boucles imbriquées, break ne sort que de la boucle interne
for i in list(1, 2, 3) {
    for j in list(1, 2, 3) {
        if j == 2 {
            break  # Sort uniquement de la boucle interne
        }
        print(i, j)
    }
}

continue - Passer à l'itération suivante

Le mot-clé continue passe directement à l'itération suivante de la boucle, en ignorant le reste du code.

# Ignorer les nombres pairs
for i in range(10) {
    if i % 2 == 0 {
        continue  # Passe à l'itération suivante
    }
    print(i)  # Affiche uniquement les impairs
}

# Filtrage dans une boucle while
i = 0
sum = 0
while i < 10 {
    i = i + 1
    if i % 3 == 0 {
        continue  # Ignore les multiples de 3
    }
    sum = sum + i
}

# Validation de données
ages = list(25, -5, 30, 150, 42)
valid_ages = list()
for age in ages {
    if age < 0 or age > 120 {
        continue  # Ignore les valeurs invalides
    }
    valid_ages = valid_ages + list(age)
}

Combiner break et continue

# Recherche avec filtrage
numbers = list(10, 20, 35, 40, 55, 60)
result = None

for num in numbers {
    if num % 10 == 0 {
        continue  # Ignore les multiples de 10
    }
    if num > 50 {
        result = num
        break  # Trouve le premier > 50 (non multiple de 10)
    }
}

result  # → 55

Notes importantes :

  • break et continue ne fonctionnent que dans les boucles (while, for)
  • Les écrire en dehors d'une boucle est refusé à la lecture du fichier, avant toute exécution : SyntaxError: 'break' outside loop
  • Dans des boucles imbriquées, ils n'affectent que la boucle la plus interne
  • Pour sortir d'une fonction, utiliser return (qui fonctionne même dans une boucle)

Une fonction est sa propre portée de contrôle de flux

Une boucle qui entoure une fonction ne lui prête pas son break, et une boucle qui l'appelle non plus. Le corps de la fonction est lu comme un territoire séparé : le break n'y trouve aucune boucle à interrompre, donc le fichier est refusé.

g = () => { break }                # SyntaxError: 'break' outside loop
for i in range(0, 3) { g() }

La règle vaut identiquement pour une fonction définie dans le corps de la boucle, pour une méthode de structure et pour un callback de broadcast — toutes sont des corps de fonction.

for i in range(0, 3) {
    g = () => { break }            # SyntaxError aussi : la boucle englobante n'est pas candidate
    g()
}

Un break qui traverserait l'appel donnerait à l'appelé le droit d'interrompre une boucle qu'il ne voit pas, écrite dans un fichier qu'il ne connaît pas. Le refus est moins un garde-fou qu'un aveu : personne ne saurait dire de quelle boucle il s'agit.

Un en-tête n'est pas dans ce qu'il introduit

Un bloc est une expression, donc il tient partout où une expression tient : dans une condition de while, dans un itérable de for, dans une valeur par défaut de paramètre. Ces trois positions sont évaluées par la portée englobante, pas par la construction qu'elles ouvrent. Un break écrit dans une condition de while ne vise donc pas ce while :

while { break; True } { 1 }        # SyntaxError: 'break' outside loop

Ce n'est pas une interdiction propre aux en-têtes : la condition s'évalue avant que la boucle n'existe, donc il n'y a rien à interrompre. Le même break dans une boucle englobante est accepté, et interrompt celle-là :

acc = 0
for i in range(0, 3) {
    acc = acc + 1
    while { break; True } { 1 }    # interrompt le for, pas le while
}
acc                                 # → 1

La règle range de la même façon le return d'une valeur par défaut, qui appartient à la signature et non au corps — voir FUNCTIONS.

Une condition de boucle est lue une fois de plus qu'on ne le croit : avant la première itération, alors que la boucle n'a pas commencé. Elle appartient à ce qui précède, pas à ce qu'elle déclenche.

Exception Handling

Voir SYNTAX.md pour la syntaxe try/except/finally/raise et les types d'exception disponibles.

Context Managers (with)

Le bloc with garantit l'appel de __exit__ en sortie, qu'elle soit normale ou provoquée par une erreur. La syntaxe est sans parenthèses, cohérente avec if, while et for :

with f = open("data.csv") {
    f.read()
}

Le binding (f) reçoit la valeur retournée par __enter__(). À la sortie du bloc, __exit__() est appelé automatiquement, même si une exception a été levée.

Multi-binding

Plusieurs context managers sont séparés par des virgules. Le cleanup se fait en ordre inverse :

with a = open("input"), b = open("output") {
    b.write(a.read())
}
# b.__exit__() appelé en premier, puis a.__exit__()

Chaque binding est visible pour les suivants : with a = expr1, b = use(a) { ... }.

Suppression d'exception

Si __exit__ retourne une valeur truthy, l'exception est supprimée :

import('contextlib')
with _ = contextlib.suppress(ValueError) {
    raise ValueError("ignored")
}
# Pas d'exception : suppress retourne True dans __exit__

Sémantique

Le with est un desugaring pur vers try/except/finally. L'exception active est passée à __exit__ sous forme (type, value, None) -- le traceback est toujours None (écart documenté avec Python).