LEÇON 5 / 60 minutes

Procédures et contrats

Une procédure donne un nom à une tâche bien délimitée. Elle reçoit des paramètres, calcule quelque chose et peut renvoyer un résultat que d’autres procédures réutilisent.

À retenir·Une bonne procédure a une responsabilité lisible, des entrées explicites et un résultat dont le type est annoncé.

La forme générale d’une procédure

proc additionner(gauche: i64, droite: i64) -> i64 {
    return gauche + droite;
}

proc introduit la déclaration. additionner est le nom de la procédure. Les noms dans les parenthèses sont des paramètres : ce sont les entrées que l’appelant fournit. Chaque paramètre a un type explicite. Après la flèche, i64 annonce le type de retour. Le corps entre accolades effectue le travail et return renvoie la valeur calculée.

On peut ensuite appeler cette procédure depuis une autre procédure :

proc total_du_panier() -> i64 {
    let sous_total: i64 = additionner(18, 24);
    return sous_total;
}

Les arguments 18 et 24 occupent les places des paramètres gauche et droite. Le nombre, l’ordre et le type des arguments doivent correspondre à la déclaration.

Exemple vérifié : paramètres, appels et retour

examples/tutorials/procedures.vit compose un sous-total puis ajoute la livraison ; le calcul attendu vaut 42.

build/bin/vitte run examples/tutorials/procedures.vit --quiet --no-color

Résultat confirmé : code de sortie 0.

Décomposer plutôt que tout entasser

Une procédure devrait répondre à une question précise. Si une fonction de 80 lignes valide une entrée, calcule plusieurs valeurs, transforme un résultat et affiche un rapport, ces responsabilités gagnent à être séparées. Des procédures courtes rendent les tests plus ciblés et les noms racontent le déroulement.

Les paramètres rendent les dépendances visibles. Une procédure qui a besoin d’un taux de remise devrait généralement le recevoir en paramètre plutôt que de dépendre d’une variable globale cachée. Le code d’appel devient alors une petite description de ce qui est calculé.

Retourner une valeur ou terminer sans valeur

Quand le type de retour est renseigné, chaque chemin normal doit produire une valeur compatible. Certaines procédures servent plutôt à effectuer une action, comme écrire un message. Selon les primitives et les conventions du module, elles peuvent avoir un type de retour sans valeur, souvent nommé void. Le point d’entrée d’exemple renvoie un entier comme statut de fin.

Évitez de mélanger plusieurs styles dans une même abstraction. Si une procédure calcule un prix, renvoie le prix ; si elle imprime un rapport, choisissez un nom d’action et laissez son effet visible dans le nom ou la documentation.

Références en paramètre

Pour examiner une valeur composée sans la copier dans chaque appel, la grammaire permet un paramètre de référence comme point: &Point. Le corps peut lire ses champs via point.x. C’est une manière de montrer que la procédure consulte une donnée existante. Les règles détaillées de mutabilité, d’alias et de durée de vie dépendent de l’implémentation disponible ; ne supposez pas un système d’emprunt plus riche que ce que le compilateur vérifie.

form Point {
    x: i64,
    y: i64,
}

proc somme(point: &Point) -> i64 {
    return point.x + point.y;
}

Contrats : écrire les préconditions et postconditions

La grammaire de Vitte permet de placer des clauses requires et ensures avant le corps d’une procédure. Une précondition décrit ce que l’appelant doit garantir. Une postcondition décrit ce que la procédure promet après son retour. Dans une postcondition, le nom result désigne la valeur produite.

proc ajouter_un(valeur: i64) -> i64
    requires valeur < 100;
    ensures result == valeur + 1;
{
    return valeur + 1;
}

Le contrat clarifie l’usage prévu et donne au compilateur des propriétés à analyser. Ici, la précondition empêche un dépassement vers la borne supérieure lors de l’ajout ; la postcondition décrit exactement le résultat promis. Une spécification doit refléter les limites du type et de l’opération. Les contrats peuvent produire des diagnostics ou des vérifications à l’exécution selon ce que le compilateur arrive à prouver.

Récursion et arrêt

Une procédure peut s’appeler elle-même. C’est la récursion. Une version récursive d’un calcul doit avoir un cas de base et un appel qui se rapproche de ce cas ; sans cela, elle ne termine pas. Pour un parcours prévisible et simple, une boucle est parfois plus facile à suivre. Choisissez la forme qui rend la condition d’arrêt la plus évidente.

Une responsabilité bien délimitée

La signature et la documentation résument ce que la procédure reçoit, ce qu’elle renvoie et les effets qu’elle produit. Lorsqu’une description contient plusieurs opérations indépendantes, plusieurs procédures nommées rendent leurs rôles plus faciles à comprendre.

Concevoir une signature avant le corps

La signature est le premier contrat lisible d’une procédure. Un nom précis décrit son rôle ; les paramètres nomment les entrées et indiquent leur type ; le type de retour annonce l’information produite. Les dépendances dont elle a besoin apparaissent comme paramètres ou comme imports visibles plutôt que comme état caché. Cette structure donne à l’appelant une description compacte de l’interface avant même la lecture du corps.

Deux paramètres du même type peuvent être intervertis sans que le compilateur puisse comprendre l’intention métier. Des noms tels que prix et quantite aident donc autant le lecteur que le diagnostic. Si l’ordre est trop facile à inverser, une forme avec des champs nommés peut rendre l’appel plus explicite.

Suivre un appel et son retour

Dans l’exemple vérifié du panier, main appelle d’abord sous_total(6, 6). La procédure renvoie 36. La procédure appelante transmet ensuite cette valeur à avec_livraison(36, 6, 6), qui renvoie 42. La valeur ne saute pas directement de la première ligne au résultat : chaque appel possède ses paramètres, son corps et son retour.

Cette lecture fournit une méthode de débogage. Notez les arguments à l’entrée de la procédure, calculez les expressions locales dans l’ordre, puis notez la valeur au return. Si le résultat final est faux, la première divergence entre cette trace et le résultat observé localise le problème.

Valeur de retour et chemins de contrôle

Pour une procédure déclarée -> i64, chaque chemin normal doit pouvoir produire un i64. Dans une condition, vérifiez toutes les branches : une branche qui ne renvoie rien peut rendre le corps incomplet. Le retour anticipé est utile quand il clarifie un cas particulier, mais trop de sorties dispersées rendent la lecture du résultat plus difficile.

Une procédure d’action n’a pas le même objectif qu’une procédure de calcul. Une procédure appelée afficher_resume annonce un effet ; une procédure appelée calculer_total annonce une valeur. Garder les noms et les types cohérents aide l’appelant à savoir s’il doit utiliser un résultat ou simplement appeler l’action.

Récursion : prouver le rapprochement du cas simple

Une procédure récursive se rappelle sur un problème plus petit. Pour calculer une somme jusqu’à n, l’idée mathématique est : la somme de 1 à 1 vaut 1 ; pour une valeur supérieure, la somme jusqu’à n est n plus la somme jusqu’à n - 1. Le cas simple empêche les appels de continuer sans fin, et chaque appel doit s’en rapprocher.

Une récursion correcte possède un cas d’arrêt, une mesure qui se rapproche de ce cas et un résultat défini pour chaque appel. La profondeur maximale et le risque de dépassement numérique font partie de ses limites. Ces propriétés décrivent l’algorithme ; elles ne garantissent pas que chaque version du backend supporte le cas précis.

Pour un calcul répétitif simple, une boucle while est souvent plus facile à tracer avec les constructions runtime confirmées. La récursion reste utile lorsque le problème est naturellement défini par des sous-problèmes, mais vérifiez la compilation avant d’en faire le fondement d’un programme.

Lecture de l’appel en chaîne

additionner(18, 24) renvoie 42 ; total_du_panier lie cette valeur à sous_total, puis la renvoie. Le nom local ne crée pas une nouvelle opération : il donne un repère à la valeur calculée par l’appel.

Une procédure définit une frontière de calcul

Une procédure sépare une responsabilité du reste du programme. Sa signature annonce le nom, les paramètres et le type de résultat ; son corps contient le calcul et les éventuels effets. Dans additionner(gauche: i64, droite: i64) -> i64, l’appelant connaît les deux entrées requises et sait qu’une valeur i64 sera renvoyée. Il n’a pas besoin de connaître la manière dont l’addition est écrite pour utiliser cette interface.

Cette frontière facilite l’évolution du code. Le corps peut être réorganisé sans changer les appels si la signature et le comportement promis restent stables. À l’inverse, modifier le nom, l’ordre des paramètres ou le type de retour touche les appelants. Des noms de paramètres précis sont particulièrement importants lorsque deux entrées ont le même type : l’analyseur peut vérifier leur ordre, mais il ne peut pas toujours deviner une inversion de sens métier.

Appel, cadre d’exécution et résultat

Un appel transmet des arguments à une nouvelle exécution du corps. Dans l’exemple additionner(18, 24), le paramètre gauche reçoit 18 et droite reçoit 24 ; l’expression du corps produit 42 ; return renvoie cette valeur à l’endroit de l’appel. Si une procédure appelle ensuite une autre procédure, les calculs s’enchaînent : le retour de l’appel intérieur devient une valeur disponible pour la procédure extérieure.

La trace conceptuelle suit donc quatre éléments : les arguments transmis, les liaisons locales, les branches parcourues et le point de retour. Elle explique pourquoi un résultat doit être suivi à travers chaque appel au lieu d’être attribué uniquement à main. Pour une procédure déclarée avec -> i64, tous les chemins qui terminent normalement doivent produire un entier compatible. Une branche qui atteint la fin sans valeur laisse la promesse de la signature incomplète.

Effets, références et récursion

Une procédure de calcul et une procédure d’action n’expriment pas la même chose. calculer_total indique un résultat ; afficher_resume annonce un effet sur une sortie. Le nom et le type devraient permettre de distinguer ces usages. Une dépendance cachée à un état global rend cette relation moins visible qu’un paramètre explicite.

Un paramètre de référence, tel que point: &Point, indique que la procédure consulte une valeur existante. Les détails de propriété, de durée de vie et d’alias ne sont pas prouvés par la simple présence de cette syntaxe ; le backend ciblé doit être pris en compte. La récursion suit un autre modèle : chaque appel crée une nouvelle étape conceptuelle qui doit se rapprocher d’un cas d’arrêt. Une mesure décroissante, un cas de base et des limites de profondeur et de représentation permettent d’expliquer ce calcul. Les boucles restent plus simples pour plusieurs répétitions linéaires, surtout lorsque leur support runtime est mieux confirmé.

Paramètre par valeur et paramètres référencés

Un paramètre typé par une valeur décrit une donnée fournie à l’appel. Une référence comme &Point décrit un accès à un objet existant. Cette différence peut influencer la copie, la propriété et la possibilité de modification, mais les garanties complètes dépendent des règles de compilation appliquées. La signature devrait rester suffisamment simple pour que l’appelant sache s’il transmet une valeur indépendante ou partage un accès à une valeur conservée ailleurs.

Une référence en lecture est adaptée lorsque la procédure consulte plusieurs champs d’un objet. Une référence mutable indique une intention d’écriture, qui impose de considérer les alias et les autres accès simultanés. Le backend natif audité rejette le cas de référence de l’exemple au stade HIR, tandis que le bootstrap l’exécute ; cette divergence doit être rattachée au chemin précis et non généralisée à toutes les signatures Vitte.

Quand scinder une procédure

Un corps se comprend mieux lorsque ses blocs correspondent à des étapes nommées du domaine. La validation d’une entrée, le calcul d’un résultat et son affichage ont des effets et des attentes différents. Des procédures séparées peuvent être appelées, examinées et vérifiées indépendamment. Une décomposition excessive, en revanche, peut disperser une opération simple entre des noms qui n’apportent aucune distinction.

Le bon découpage préserve une chaîne de responsabilités : l’appelant fournit des valeurs, la procédure les interprète selon son contrat, puis elle renvoie un résultat ou réalise un effet annoncé. Les types et noms de paramètres constituent la documentation de cette chaîne. Les clauses requires et ensures ajoutent des propriétés formelles lorsqu’elles sont supportées, mais ne rendent pas automatiquement claire une responsabilité qui n’est pas définie.