É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.rset / 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
fnsuivi du nom de la fonctioncalculer_perimetre;- les conventions de nommage Rust recommandent le
snake_case(mots séparés par un_) pour les noms de variables et de fonctions ;
- les conventions de nommage Rust recommandent le
(cote: u32)est la partie paramètres d’entrée de la fonction. Notre fonction a besoin d’un paramètrecote, de typeu32.u32est le type entier non signé (positif ou nul) de 32 bits.
-> u64est le type du retour de notre fonction.u64est 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
- J’utilise cette astuce pour éviter un éventuel dépassement du maximum possible en
{}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 * 4est 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.
- Dans une fonction Rust, la dernière expression non suivie d’un
pubindique 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 typeu64, 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) enu32, 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
- gaspiller la moitié des capacités d’encodage sur 32 bits ;
- 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.