Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Écriture de notre fonction

Nous allons écrire le code d’une fonction qui calcule le périmètre d’un carré.

Nous devons d’abord apprendre comment s’organise un projet (crate) Rust : il se compose d’une librairie (ou API) et / ou d’un programme exécutable.

Librairie, programme

Dans un projet (crate) Rust, nous disposons généralement d’une partie API (librairie) et d’une partie programme.

Par convention, Cargo s’attend à trouver :

  • le code source d’un programme dans src/main.rs et / ou
  • le code source d’une librairie dans src/lib.rs.

Ceci est le comportement simple par défaut. Nous verrons par la suite comment donner des indications au compilateur pour utiliser d’autres fichiers et / ou comment ajouter des composants internes et externes au projet.

Mise en application

Nous créons un nouveau projet mon_api_maths en dehors de tout autre projet (dans ~/mesprojets dans mon cas).

Nous y créons le fichier src/lib.rs et écrivons notre code source, à commencer par la fonction souhaitée.

Code source sans documentation

Note au sujet des notations

Je m’assure ici et pendant quelques temps d’utiliser des notations de typage complètes et strictes.
Cela garantit une parfaite lisibilité EXPLICITE des codes afin que vous distinguiez mieux les types utilisés. 1
Ceci vous permet également d’apprendre ces conventions d’écriture.

Rassurez-vous, Rust pratique couramment l’inférence de type et nous pourrons nous passer de cette rigueur par la suite.
Ce d’autant plus que, comme nous le verrons, le compilateur Rust nous prémunit de toute erreur de typage à la compilation : si le code compile, c’est que nos types correspondent bien ou ont bien été inférés par le compilateur. Nous n’aurons pas de mauvaise surprise sur ce plan lors de l’exécution de nos programmes.

Les notations cote_u64: u64 = et 4_u64 ci-après ne sont absolument pas indispensables grâce à l’inférence de type. Il est toutefois utile de les connaître et de prendre l’habitude de « mentaliser » le type d’une valeur littérale lorsque nous l’écrivons ou la lisons dans nos codes Rust.

Code de notre fonction

#![allow(unused)]
fn main() {
pub fn calculer_perimetre(cote: u32) -> u64 {
    cote_u64: u64 = cote as u64;
    cote_u64 * 4_u64
}
}

Apprentissages

Cette fonction nous permet d’apprendre quelques notions de base essentielles :

  • Nous déclarons une fonction avec le mot-clef fn suivi du nom de la fonction calculer_perimetre ;
    • les conventions de nommage Rust recommandent le snake_case (mots séparés par un _) pour les noms de variables et de fonctions ;
  • (cote: u32) est la partie paramètres d’entrée de la fonction. Notre fonction a besoin d’un paramètre cote, de type u32.
    • u32 est le type entier non signé (positif ou nul) de 32 bits.
  • -> u64 est le type du retour de notre fonction.
    • u64 est le type entier non signé (positif ou nul) de 64 bits.
      • J’utilise cette astuce pour éviter un éventuel dépassement du maximum possible en u32. 1
  • {} les accolades délimitent ici le corps de notre fonction. Plus généralement, nous pouvons définir des blocs par des accolades à tout endroit dans nos codes Rust.
  • cote_u64 * 4 est une expression qui calcule ET constitue le retour de notre fonction.
    • Dans une fonction Rust, la dernière expression non suivie d’un ; est, par convention, le retour de cette fonction.
  • pub indique que nous souhaitons rendre la fonction disponible et accessible à tout programme qui le souhaite.

Nous le verrons, Rust n’autorise pas les opérations sur des variables de types différents.
C’est la raison pour laquelle, si je souhaite renvoyer un résultat de type u64 , je dois d’abord convertir l’argument passé à ma fonction dans une variable du même type.
Je le fais avant le calcul. Si je le faisais sur une valeur calculée qui serait (cote * 4) en u32, je perdrais tout l’intérêt de cette astuce préventive. 1

Notes d'approfondissement

Signature de notre fonction

Pour des raisons que nous aborderons plus tard, Rust distingue certains types simples dont les entiers font partie. Nous passons donc bien la valeur en argument et non une référence, sans pour autant pénaliser le code appelant notre fonction.

Type par défaut

Le compilateur Rust utilise l’inférence de type et associe des types par défaut aux variables en fonction de ce qu’il connaît et observe de nos codes sources.

De fait, le compilateur Rust — qui ne dispose pas « d’intelligence métier » — ne peut pas se douter que la longueur de côté d’un carré est nécessairement un entier positif ou nul (unsigned), idem pour le périmètre. Il ne sait même pas ce qu’est un « côté » ni un « carré » !

Le type « entier » par défaut de Rust est i32. C’est le type qui serait utilisé si nous ne précisions pas explicitement u32 pour le paramètre ni u64 pour le résultat ni… (le compilateur Rust analyse le code de manière plus profonde encore…).

Nous, nous savons que ce serait un non-sens d’utiliser i32.
Ce serait

  1. gaspiller la moitié des capacités d’encodage sur 32 bits ;
  2. risquer de créer une erreur conceptuelle grave dans le code en acceptant ou en générant des valeurs négatives sans que ce comportement ne soit interdit par notre code.

Il est donc important et intelligent de choisir un entier non signé dans ce cas.

Nous nous servons du typage statique fort de Rust pour nous « protéger ».

Je croise souvent des codes qui utilisent des types entiers signés alors que la « logique métier » impose des valeurs nécessairement positives ou nulles. Parfois par omission, parfois explicitement.
Dans le premier cas (laisser le compilateur choisir), c’est une faute grave.
Dans le second (demander explicitement un entier signé dans un contexte métier comme celui-ci), c’est une faute lourde.
On peut clairement se demander si nous ferions confiance à un·e développeur·se qui commet ce genre d’impair pour un code qui nous concerne personnellement (Spoiler : moi, non !)

Astuce de protection

L’astuce de protection que j’utilise en retournant un résultat u64 est un pis-aller.

Je m’en sers pour vous sensibiliser à l’importance de réfléchir à ce qui peut se produire avec nos codes.
Ici, nous pouvons stocker une valeur maximale u32 de 4_294_967_295. Si nous appelons la fonction avec une valeur de cote supérieure à 1_073_741_823, nous dépasserons la limite dans le résultat du calcul du périmètre.

J’ai choisi, dans cette première partie d’apprentissage, de rester au plus simple. Dans un code robuste, nous mettrions en place des méthodes de contrôle et de protection pour nous prémunir de cas aux limites et des bogues résiduels liés à ce débordement.

L’intérêt de passer en u64 est que je suis sûr que la valeur de périmètre retournée est dans la limite des valeurs possibles et que ma fonction ne provoquera pas de problème (avertissement, erreur ou valeur incohérente).

Écriture plus idiomatique

J’ai choisi l’écriture ci-avant pour une meilleure lisibilité du code. C’est un choix pédagogique et didactique.

Dans la vraie vie, nous pouvons raccourcir le code et éviter la création d’une variable inutile :

#![allow(unused)]
fn main() {
pub fn calculer_perimetre(cote: u32) -> u64 {
    (cote as u64) * 4_u64
}
}

J’utiliserai indifféremment l’une ou l’autre version dans la suite.


  1. Oui, je suis taquin et pointilleux. ↩2 ↩3