Aller au contenu

Les modules en Rust

Jusqu’ici, tous les exemples tenaient dans un seul fichier main.rs. C’est très bien pour de petits programmes, mais les vrais projets découpent leur code en morceaux logiques. En Rust, l’outil pour ça est le système de modules : mod, pub et use.

Un module est un bloc nommé qui regroupe du code qui va ensemble :

mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
}
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}

Sortie :

5
▶ Essayez par vous-même

Deux choses à remarquer. D’abord, on appelle la fonction avec math::add(...), en utilisant le nom du module comme chemin. Ensuite, elle est marquée pub : sans ce mot-clé, on ne pourrait pas l’atteindre depuis l’extérieur du module.

Ce point mérite d’être souligné, car il surprend souvent : le contenu d’un module est privé par défaut. Si vous retirez pub de l’exemple ci-dessus, le code ne compile plus :

mod math {
fn add(a: i32, b: i32) -> i32 { // pas de `pub`
a + b
}
}
fn main() {
let sum = math::add(2, 3); // ❌ erreur : function `add` is private
println!("{sum}");
}

Le compilateur répond « function add is private » : la fonction add est privée. Tout ce que vous voulez rendre utilisable en dehors du module doit être marqué pub explicitement. C’est voulu : un module peut ainsi cacher ses fonctions d’aide internes et n’exposer que les parties qui forment son API publique.

Un module peut contenir d’autres modules. C’est comme ça que les grands projets construisent une structure en arbre :

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
fn main() {
let area = shapes::circle::area(2.0);
println!("{area}");
}
▶ Essayez par vous-même

Écrire shapes::circle::area(...) à chaque fois devient vite pénible. use fait entrer un chemin dans la portée (scope) pour que vous puissiez l’utiliser directement :

mod shapes {
pub mod circle {
pub fn area(radius: f64) -> f64 {
3.14159 * radius * radius
}
}
}
use shapes::circle;
fn main() {
let area = circle::area(2.0); // plus court que shapes::circle::area
println!("{area}");
}
▶ Essayez par vous-même

En fait, vous avez déjà utilisé use : c’est le même mot-clé que sur la page HashMap (use std::collections::HashMap;).

Les exemples ci-dessus mettent tout dans un seul fichier, car le Rust Playground l’exige. Dans un vrai projet sur votre machine, chaque module a en général son propre fichier. Un projet avec un module math ressemblerait typiquement à ceci :

src/
├── main.rs
└── math.rs
src/main.rs
mod math; // dit à Rust : le code de ce module se trouve dans math.rs
fn main() {
let sum = math::add(2, 3);
println!("{sum}");
}
src/math.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}

mod math; (avec un point-virgule et sans corps) dit à Rust de charger le contenu du module depuis le fichier math.rs du même dossier. C’est le schéma que vous utiliserez dès qu’un projet dépassera un seul fichier.

Créez un module convert avec deux fonctions publiques : c_to_f (Celsius vers Fahrenheit) et f_to_c (dans l’autre sens). Depuis main, affichez 100°C font 212°F et 212°F font 100°C.

Indice : les formules sont c * 9.0 / 5.0 + 32.0 et (f - 32.0) * 5.0 / 9.0. N’oubliez pas pub, et appelez les fonctions avec le nom du module, par exemple convert::c_to_f(100.0).

Voir la solution
mod convert {
pub fn c_to_f(c: f64) -> f64 {
c * 9.0 / 5.0 + 32.0
}
pub fn f_to_c(f: f64) -> f64 {
(f - 32.0) * 5.0 / 9.0
}
}
fn main() {
println!("100°C font {}°F", convert::c_to_f(100.0));
println!("212°F font {}°C", convert::f_to_c(212.0));
}

Sortie :

100°C font 212°F
212°F font 100°C
▶ Essayez par vous-même

Les deux fonctions sont pub, donc main (qui se trouve en dehors du module) peut les appeler. Retirez un pub et le compilateur vous dira que cette fonction est privée.

Créez un module imbriqué school::grades avec une fonction publique letter(score: u32) -> char qui renvoie 'A' à partir de 90, 'B' à partir de 80, et 'C' sinon. Dans grades, écrivez aussi une fonction d’aide privée is_at_least(score: u32, min: u32) -> bool que letter utilise. Faites entrer school::grades dans la portée avec use, puis affichez la lettre pour 95, 85 et 70.

Indice : le mod grades à l’intérieur de school et la fn letter ont tous deux besoin de pub, mais pas is_at_least. Le code d’un module peut toujours appeler les fonctions privées de ce même module.

Voir la solution
mod school {
pub mod grades {
pub fn letter(score: u32) -> char {
if is_at_least(score, 90) {
'A'
} else if is_at_least(score, 80) {
'B'
} else {
'C'
}
}
// privée : seul le code de `grades` peut l'appeler
fn is_at_least(score: u32, min: u32) -> bool {
score >= min
}
}
}
use school::grades;
fn main() {
for score in [95, 85, 70] {
println!("{score} -> {}", grades::letter(score));
}
}

Sortie :

95 -> A
85 -> B
70 -> C
▶ Essayez par vous-même

letter peut appeler is_at_least parce qu’elles vivent dans le même module. Depuis main, grades::is_at_least(95, 90) échouerait avec « function is_at_least is private ». C’est tout l’intérêt : le module montre une petite API publique et garde ses fonctions d’aide pour lui.

  • mod nom { ... } regroupe du code lié dans un module nommé.
  • Tout ce qui se trouve dans un module est privé par défaut : ajoutez pub pour l’exposer à l’extérieur.
  • use fait entrer un chemin dans la portée, pour ne pas avoir à l’écrire en entier à chaque fois.
  • Dans un projet à plusieurs fichiers, mod nom; (sans corps) charge le code du module depuis le fichier correspondant.
  • Le code d’un module peut toujours utiliser les éléments privés de ce module : gardez les fonctions d’aide privées et n’exposez que ce dont les appelants ont besoin.

Petit quiz

1. Par défaut, les éléments d'un mod sont-ils publics ou privés ?

2. Comment dire à Rust de charger le code d'un module depuis math.rs au lieu de l'écrire directement ?

Score : 0 / 2