exercices_introbases1bases2types_avancesgen_types_composesproprietemodules_visibilitetoolinggestion_erreursclosuresiterateurscollectionslifetimescliunsafeexercices_avances

Exercices introductifs

Le but de ces petits exercices est d'écrire des programmes simples avec une connaissance minimale du Rust. Faites-les dans l'ordre qui vous amuse. N'hésitez pas à refaire plusieurs fois le même exercice en variant le type de structures de contrôle que vous utilisez par exemple.

Année bissextile

Écrire un programme déterminant si une année est bissextile. Pour savoir si une année est bissextile, il faut que:

  • l'année soit divisible par 4,
  • sauf si elle est divisible par 100,
  • sauf si elle est aussi divisible par 400!

Ainsi, l'année 2001 n'est pas bissextile. En revanche l'année 2000 l'est. Contrairement à l'année 1900 qui ne l'est pas.

  • Objectif : if/else, opérateur %, booléens (bases du Rust).
  • Signature suggérée : fn est_bissextile(annee: u32) -> bool.
  • Cas de test : est_bissextile(2000) -> true, est_bissextile(1900) -> false, est_bissextile(2001) -> false, est_bissextile(2024) -> true.

Nombre d'Armstrong

Un nombre d'Armstrong est un nombre qui est la somme de ses propres chiffres, chacun élevé à la puissance du nombre de chiffres.

Par exemple :

  • \(9\) est un nombre d'Armstrong, car \(9 = 9^1 = 9\),
  • \(10\) n'est pas un nombre d'Armstrong, car \(10 \neq 1^2 + 0^2 = 1\),
  • \(153\) est un nombre d'Armstrong, car : \(153 = 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153\),
  • \(154\) n'est pas un nombre d'Armstrong, car : \(154 \neq 1^3 + 5^3 + 4^3 = 1 + 125 + 64 = 190\)

Écrivez un programme qui détermine si un nombre est un nombre d'Armstrong.

  • Objectif : décomposition d'un nombre en chiffres (% 10, / 10), boucle, u32::pow (bases du Rust).
  • Signature suggérée : fn est_armstrong(n: u32) -> bool.
  • Cas de test : est_armstrong(9) -> true, est_armstrong(10) -> false, est_armstrong(153) -> true, est_armstrong(154) -> false, est_armstrong(9474) -> true.

Nombres heureux

Un nombre heureux est un entier naturel non nul, qui lorsqu'on calcule la somme des carrés de ses chiffres puis la somme des carrés des chiffres du nombre obtenu et ainsi de suite, on aboutit au nombre 1. Un nombre est malheureux lorsque ce n'est pas le cas.

Ainsi, le nombre 7 est heureux, car:

$$ 7^2=49, $$ $$ 4^2+9^2=97, $$ $$ 9^2+7^2=130, $$ $$ 1^2+3^2+0^2=10, $$ $$ 1^2+0^2=1. $$

D'autres nombres heureux sont 1, 10, 13, 19 par exemple. Il faut faire attention car certains nombres rencontrés dans la suite mènent à une suite périodique: 4, 16, 20, 37, 42, 58, 89, 145 et on ne s'arrête jamais.

Écrivez un programme qui détermine si un nombre est heureux ou malheureux.

  • Objectif : boucle avec condition d'arrêt, détection de cycle (mémoriser les valeurs déjà vues, ou s'arrêter dès qu'on atteint 4), fonctions (bases du Rust).
  • Signature suggérée : fn est_heureux(n: u32) -> bool.
  • Cas de test : est_heureux(7) -> true, est_heureux(1) -> true, est_heureux(19) -> true, est_heureux(2) -> false, est_heureux(4) -> false.

Différence de nombres

Écrivez un programme qui calcule la différence entre le carré de la somme et la somme des carrés des $N$ premiers nombres naturels.

Le carré de la somme des dix premiers entiers naturels est $$ (1 + 2 + ... + 10)^2 = 55^2 = 3025. $$

La somme des carrés des dix premiers nombres naturels est $$ 1^2 + 2^2 + ... + 10^2 = 385. $$

Donc, la différence entre le carré de la somme des dix premiers nombres naturels et la somme des carrés des dix premiers nombres naturels est $$ 3025 - 385 = 2640. $$

  • Objectif : boucle for et accumulateurs, ou itérateurs ((1..=n).sum(), (1..=n).map(|i| i * i).sum()).
  • Signature suggérée : fn difference(n: u32) -> u64.
  • Cas de test : difference(10) -> 2640, difference(1) -> 0, difference(100) -> 25164150.

Nombre premier

Écrire un programme qui détermine si un nombre est premier. Un nombre est premier s'il n'a que deux diviseurs distincts.

  • Objectif : boucle, %, sortie anticipée (return / break) ; optimisation possible : ne tester les diviseurs que jusqu'à la racine carrée de n (bases du Rust).
  • Signature suggérée : fn est_premier(n: u32) -> bool.
  • Cas de test : est_premier(2) -> true, est_premier(1) -> false, est_premier(7) -> true, est_premier(15) -> false, est_premier(97) -> true.

Décomposition en nombres premiers

Écrire un programme qui calcule les facteurs premiers d'un nombre naturel donné.

Exemple d'algorithme

Quels sont les facteurs premiers de 60 ?

  • Notre premier diviseur est 2:
    • 60 est divisible par 2 et il reste 30,
    • 30 est divisible par 2 et il reste 15,
    • 15 n'est pas divisible par 2,
    • 15 est divisible par 3 et il reste 5,
    • 5 n'est pas divisible par 2,
    • 5 n'est pas divisible par 3,
    • 5 n'est pas divisible par 4,
    • 5 est divisible par 5 et il reste 1 et nous avons fini.

Nos diviseurs réussis dans ce calcul représentent la liste des facteurs premiers de 60 : 2, 2, 3 et 5. Si cela vous amuse vous pouvez vérifier qu'ils sont bien premiers en utilisant l'exercice précédent.

  • Objectif : boucle while, %, construction d'un Vec (voir "Les collections") - ou simple affichage au fil de l'eau.
  • Signature suggérée : fn facteurs_premiers(n: u32) -> Vec<u32>.
  • Cas de test : facteurs_premiers(60) -> [2, 2, 3, 5], facteurs_premiers(13) -> [13], facteurs_premiers(1) -> [], facteurs_premiers(360) -> [2, 2, 2, 3, 3, 5].

Les bases du Rust 1

Ces exercices portent sur les variables, la mutabilité, les constantes, les tableaux statiques et les structures de contrôle if / for. Faites-les dans l'ordre, du plus simple au plus complexe. N'utilisez pas de fonctions, contentez-vous d'afficher les valeurs.

Signe d'un nombre

Renvoyer -1 si le nombre est négatif, 0 s'il est nul, 1 s'il est positif.

  • Objectif : if / else if / else, valeur de retour.
  • Signature suggérée : -
  • Cas de test : signe(-8) -> -1, signe(0) -> 0, signe(42) -> 1.

Compte à rebours

Afficher les entiers de 10 à 1, un par ligne, puis "Décollage !".

  • Objectif : boucle for, plage 1..=10, println!.
  • Signature suggérée : -
  • Cas de test : la sortie affiche 10, 9, ..., 1, puis Décollage !.

Moyenne d'un tableau

Calculer la moyenne des cinq nombres d'un tableau [f64; 5].

  • Objectif : tableau statique, for sur les éléments, accumulateur.
  • Signature suggérée : -
  • Cas de test : moyenne([2.0, 4.0, 4.0, 4.0, 6.0]) -> 4.0, moyenne([1.0, 2.0, 3.0, 4.0, 5.0]) -> 3.0.

Min et max

Afficher le plus petit et le plus grand élément d'un tableau [i32; 8], sous la forme (min, max).

  • Objectif : for, comparaisons, mut.
  • Signature suggérée : -
  • Cas de test : min_max([4, 1, 7, 3, 9, 2, 8, 5]) -> (1, 9), min_max([0, 0, 0, 0, 0, 0, 0, 0]) -> (0, 0).

FizzBuzz

Pour les entiers de 1 à 100 : afficher "FizzBuzz" si le nombre est divisible par 3 et par 5, sinon "Fizz" s'il est divisible par 3, sinon "Buzz" s'il est divisible par 5, sinon le nombre lui-même.

  • Objectif : for, %, if / else if (ordre des tests).
  • Signature suggérée : -.
  • Cas de test : ligne 3 -> Fizz, ligne 5 -> Buzz, ligne 15 -> FizzBuzz, ligne 7 -> 7.

Les bases du Rust 2

Ces exercices portent sur les fonctions, le if utilisé comme expression, la boucle for sur des valeurs et le mot-clé return. Faites-les dans l'ordre.

Valeur absolue

Renvoyer la valeur absolue d'un entier, en utilisant le if comme expression (pas de return, pas de mut).

  • Objectif : let x = if cond { a } else { b };.
  • Signature suggérée : fn abs(n: i64) -> i64.
  • Cas de test : abs(-5) -> 5, abs(5) -> 5, abs(0) -> 0.

Factorielle

Calculer n! (produit des entiers de 1 à n). Par convention 0! = 1.

  • Objectif : for, accumulateur.
  • Signature suggérée : fn factorielle(n: u64) -> u64.
  • Cas de test : factorielle(0) -> 1, factorielle(1) -> 1, factorielle(5) -> 120, factorielle(10) -> 3628800.

PGCD (algorithme d'Euclide)

Calculer le plus grand commun diviseur de deux entiers par modulos successifs.

  • Objectif : while, %, réaffectation de variables.
  • Signature suggérée : fn pgcd(a: u64, b: u64) -> u64.
  • Cas de test : pgcd(12, 18) -> 6, pgcd(17, 5) -> 1, pgcd(0, 7) -> 7, pgcd(48, 36) -> 12.

Puissance sans pow

Calculer base élevé à la puissance exp, sans utiliser i64::pow ni f64::powi.

  • Objectif : for, accumulateur multiplicatif.
  • Signature suggérée : fn puissance(base: i64, exp: u32) -> i64.
  • Cas de test : puissance(2, 0) -> 1, puissance(2, 10) -> 1024, puissance(-3, 3) -> -27.

Nombre de chiffres

Compter les chiffres de l'écriture décimale d'un entier. 0 compte pour un chiffre.

  • Objectif : while, division entière /= 10.
  • Signature suggérée : fn nb_chiffres(n: u64) -> u32.
  • Cas de test : nb_chiffres(0) -> 1, nb_chiffres(7) -> 1, nb_chiffres(1000) -> 4, nb_chiffres(123456789) -> 9.

Les types avancés

Ces exercices portent sur les types énumérés, les fonctions associées et les méthodes, ainsi que le pattern matching. Faites-les dans l'ordre.

Feu tricolore

Définir enum Feu { Rouge, Orange, Vert } et une fonction qui renvoie l'état suivant du feu : Vert -> Orange -> Rouge -> Vert.

  • Objectif : enum sans données, match.
  • Signature suggérée : fn suivant(f: Feu) -> Feu.
  • Cas de test : suivant(Feu::Vert) -> Feu::Orange, suivant(Feu::Orange) -> Feu::Rouge, suivant(Feu::Rouge) -> Feu::Vert.

Point cardinal

Définir enum Direction { Nord, Est, Sud, Ouest } et une fonction qui fait tourner d'un quart de tour vers la droite : Nord -> Est -> Sud -> Ouest -> Nord.

  • Objectif : enum, match.
  • Signature suggérée : fn tourner_droite(d: Direction) -> Direction.
  • Cas de test : tourner_droite(Direction::Nord) -> Direction::Est, tourner_droite(Direction::Ouest) -> Direction::Nord.

Forme géométrique

Définir enum Forme { Cercle(f64), Rectangle(f64, f64), Triangle { base: f64, hauteur: f64 } } et une méthode aire(&self) -> f64.

  • Objectif : variantes avec données (tuple et struct), match avec liaison de valeurs, méthode prenant &self.
  • Signature suggérée : impl Forme { fn aire(&self) -> f64 { /* ... */ } }.
  • Cas de test : Forme::Rectangle(3.0, 4.0).aire() -> 12.0, Forme::Triangle { base: 6.0, hauteur: 2.0 }.aire() -> 6.0, Forme::Cercle(1.0).aire() -> environ 3.14159.

Pierre-feuille-ciseaux

Définir enum Coup { Pierre, Feuille, Ciseaux } et enum Issue { Gagne, Perd, Nul }. Écrire une fonction qui donne l'issue du premier joueur.

  • Objectif : match sur un tuple (a, b).
  • Signature suggérée : fn jouer(a: Coup, b: Coup) -> Issue.
  • Cas de test : jouer(Coup::Pierre, Coup::Ciseaux) -> Issue::Gagne, jouer(Coup::Pierre, Coup::Feuille) -> Issue::Perd, jouer(Coup::Feuille, Coup::Feuille) -> Issue::Nul.

Week-end

Définir enum Jour { Lundi, Mardi, Mercredi, Jeudi, Vendredi, Samedi, Dimanche } et une fonction qui dit si c'est le week-end.

  • Objectif : match avec motifs multiples (Jour::Samedi | Jour::Dimanche) ou _.
  • Signature suggérée : fn est_weekend(j: Jour) -> bool.
  • Cas de test : est_weekend(Jour::Samedi) -> true, est_weekend(Jour::Dimanche) -> true, est_weekend(Jour::Lundi) -> false.

La généricité et des types composés

Ces exercices portent sur les types génériques, les traits, les tuples et les attributs #[derive]. Faites-les dans l'ordre.

Maximum générique

Écrire une fonction qui renvoie le plus grand de deux éléments, quel que soit leur type - du moment qu'on peut les comparer.

  • Objectif : paramètre générique <T>, contrainte T: PartialOrd.
  • Signature suggérée : fn maximum<T: PartialOrd>(a: T, b: T) -> T.
  • Cas de test : maximum(3, 8) -> 8, maximum(2.5, 1.0) -> 2.5, maximum("abc", "abd") -> "abd".

Paire générique

Définir struct Paire<T> { premier: T, second: T } et une fonction qui renvoie une nouvelle paire avec les deux champs échangés.

  • Objectif : struct générique, fonction (ou méthode) générique.
  • Signature suggérée : fn echanger<T>(p: Paire<T>) -> Paire<T>.
  • Cas de test : echanger(Paire { premier: 1, second: 2 }) -> Paire { premier: 2, second: 1 }.

Trait Aire

Définir trait Aire { fn aire(&self) -> f64; } et l'implémenter pour struct Cercle { rayon: f64 } et struct Carre { cote: f64 }.

  • Objectif : définition d'un trait, impl Trait for Type.
  • Signature suggérée : trait Aire { fn aire(&self) -> f64; }.
  • Cas de test : Carre { cote: 3.0 }.aire() -> 9.0, Cercle { rayon: 2.0 }.aire() -> environ 12.566.

Trait avec méthode par défaut

Définir trait Decrire avec une méthode nom(&self) -> String (obligatoire) et une méthode decrire(&self) -> String par défaut qui renvoie format!("Ceci est {}", self.nom()). Implémenter le trait pour un premier type en ne fournissant que nom, puis pour un second type en redéfinissant aussi decrire.

  • Objectif : méthode par défaut d'un trait, redéfinition.
  • Signature suggérée : trait Decrire { fn nom(&self) -> String; fn decrire(&self) -> String { /* défaut */ } }.
  • Cas de test : un Chat qui ne définit que nom (renvoyant "un chat") -> decrire() = "Ceci est un chat" ; un type qui redéfinit decrire -> sa propre chaîne.

Point<T> et somme des coordonnées

Définir struct Point<T> { x: T, y: T } et une méthode somme(&self) -> T qui renvoie x + y, disponible seulement lorsque T supporte l'addition.

  • Objectif : impl<T: ...> avec contrainte (std::ops::Add<Output = T> + Copy).
  • Signature suggérée : impl<T: Add<Output = T> + Copy> Point<T> { fn somme(&self) -> T }.
  • Cas de test : Point { x: 3, y: 4 }.somme() -> 7, Point { x: 1.5, y: 2.5 }.somme() -> 4.0.

La propriété

Ces exercices portent sur la liaison et le déplacement (move), les emprunts partagés (&) et mutables (&mut) et le déréférencement (*). Faites-les dans l'ordre.

Somme sans consommer

Calculer la somme des éléments d'un vecteur sans en prendre la propriété : après l'appel, le vecteur doit toujours être utilisable par l'appelant.

  • Objectif : &[i32] en argument, for sur des références.
  • Signature suggérée : fn somme(v: &[i32]) -> i32.
  • Cas de test : somme(&vec![1, 2, 3, 4]) -> 10, somme(&[]) -> 0.

Doubler en place

Multiplier par 2 chaque élément d'un vecteur, en le modifiant sur place.

  • Objectif : &mut [i32], for x in v.iter_mut(), *x.
  • Signature suggérée : fn doubler(v: &mut [i32]).
  • Cas de test : après doubler(&mut v) avec v = [1, 2, 3], v == [2, 4, 6].

Échanger deux entiers

Échanger les valeurs pointées par deux références mutables, sans utiliser std::mem::swap.

  • Objectif : &mut i32, déréférencement *, variable temporaire.
  • Signature suggérée : fn echanger(a: &mut i32, b: &mut i32).
  • Cas de test : après echanger(&mut x, &mut y) avec x = 1, y = 2, on a x == 2 && y == 1.

Corriger le code

On te donne un extrait qui ne compile pas : une valeur y est utilisée après avoir été déplacée. Corrige-le sans supprimer de ligne (indice : un emprunt, ou un .clone()).

  • Objectif : lire un message d'erreur du borrow checker, choisir entre & et .clone().
  • Signature suggérée : -
  • Cas de test : le code compile et affiche deux fois la même valeur.

Compter les pairs

Compter combien d'éléments d'un vecteur sont pairs, sans consommer le vecteur.

  • Objectif : &[i32], for, %, compteur mut.
  • Signature suggérée : fn nb_pairs(v: &[i32]) -> usize.
  • Cas de test : nb_pairs(&[1, 2, 3, 4, 6]) -> 3, nb_pairs(&[1, 3, 5]) -> 0.

Modules et visibilité

Ces exercices portent sur les modules (mod), la visibilité (pub), les chemins (crate::, super::), les sous-modules et les modules en ligne. Faites-les dans l'ordre.

Découper en modules

On te donne un petit programme où sont mélangées, dans main.rs, les fonctions aire_rectangle, perimetre_rectangle, km_en_miles et miles_en_km. Réorganise-le en deux modules-fichiers, geometrie et conversions, en ne mettant en pub que l'API utile.

  • Objectif : mod nom; + fichier, pub fn, chemins geometrie::aire_rectangle(...).
  • Signature suggérée : -
  • Cas de test : main peut appeler geometrie::aire_rectangle(3.0, 4.0) -> 12.0 et conversions::km_en_miles(10.0) -> environ 6.21.

Module en ligne

Définir, directement dans main.rs, mod maths { ... } contenant une fonction publique carre(x: i64) -> i64 et une fonction privée utilisée en interne. Appeler carre depuis main.

  • Objectif : mod nom { ... }, visibilité privée par défaut.
  • Signature suggérée : mod maths { pub fn carre(x: i64) -> i64 { /* ... */ } }.
  • Cas de test : maths::carre(5) -> 25 ; appeler la fonction privée depuis main doit provoquer une erreur de compilation.

Corriger la visibilité

On te donne un module formes avec struct Point { x: f64, y: f64 } et une fonction, tous privés. Le main ne compile pas. Ajoute les pub nécessaires (sur le type, sur ses champs, sur la fonction) pour que ça compile.

  • Objectif : comprendre que pub struct ne rend pas les champs publics.
  • Signature suggérée : -
  • Cas de test : main peut écrire formes::Point { x: 1.0, y: 2.0 } et appeler la fonction.

Sous-module et pub use

Créer banque::comptes::Compte (dans banque/comptes.rs ou en ligne). Ajouter dans banque un pub use comptes::Compte; pour pouvoir écrire banque::Compte depuis l'extérieur.

  • Objectif : sous-modules, ré-export avec pub use.
  • Signature suggérée : -
  • Cas de test : main accède au type via banque::Compte et via banque::comptes::Compte.

Les petits trucs sympas qui aident au développement

Ces exercices portent sur les commentaires de documentation, les tests unitaires, assert_eq!, les tests de documentation, rustfmt et clippy. Faites-les dans l'ordre.

Écrire des tests

Reprends factorielle et pgcd des exercices précédents. Écris un module #[cfg(test)] mod tests avec, pour chaque fonction, au moins trois assert_eq! dont un cas limite (0, entrée valant 1, ...).

  • Objectif : #[cfg(test)], #[test], use super::*;, assert_eq!.
  • Signature suggérée : -
  • Cas de test : cargo test affiche tous les tests au vert.

Test de documentation

Documente fn celsius_en_fahrenheit(c: f64) -> f64 avec un commentaire /// contenant un bloc de code exemple, qui sera exécuté comme test par cargo test.

  • Objectif : doc-test (``` ... ``` dans un ///).
  • Signature suggérée : fn celsius_en_fahrenheit(c: f64) -> f64.
  • Cas de test : l'exemple assert_eq!(celsius_en_fahrenheit(100.0), 212.0); passe ; celsius_en_fahrenheit(0.0) -> 32.0.

#[should_panic]

Écris fn racine_entiere(n: i64) -> i64 qui panique si n < 0, et un test annoté #[should_panic] qui le vérifie.

  • Objectif : panic!, #[should_panic], #[should_panic(expected = "...")].
  • Signature suggérée : fn racine_entiere(n: i64) -> i64.
  • Cas de test : racine_entiere(9) -> 3, racine_entiere(-1) panique.

Nettoyer avec clippy

On te donne un extrait volontairement non idiomatique : if x == true, if cond { true } else { false }, for i in 0..v.len() { /* ... v[i] ... */ }. Lance cargo clippy et applique ses suggestions.

  • Objectif : lire les lints de clippy et écrire du code idiomatique.
  • Signature suggérée : -
  • Cas de test : cargo clippy ne remonte plus d'avertissement sur cet extrait.

La gestion des erreurs en Rust

Ces exercices portent sur Option, Result, panic! et l'opérateur ?. Faites-les dans l'ordre.

Division sûre

Diviser deux entiers, en renvoyant une erreur explicite si le diviseur est nul.

  • Objectif : Result<T, E>, variantes Ok / Err.
  • Signature suggérée : fn div(a: i32, b: i32) -> Result<i32, String>.
  • Cas de test : div(10, 2) -> Ok(5), div(7, 0) -> Err(...) (message au choix).

Premier élément

Renvoyer le premier élément d'un slice, ou rien s'il est vide - sans indexer directement (v[0] paniquerait sur un slice vide).

  • Objectif : Option<T>, .first() ou match.
  • Signature suggérée : fn premier(v: &[i32]) -> Option<i32>.
  • Cas de test : premier(&[3, 1, 2]) -> Some(3), premier(&[]) -> None.

Parser une durée "mm:ss"

Convertir une chaîne de la forme "mm:ss" en nombre de secondes. Renvoyer None si le format est invalide.

  • Objectif : str::split, str::parse (qui renvoie un Result), passage Result -> Option avec .ok(), opérateur ? sur une Option.
  • Signature suggérée : fn parse_mmss(s: &str) -> Option<u32>.
  • Cas de test : parse_mmss("03:45") -> Some(225), parse_mmss("00:00") -> Some(0), parse_mmss("bonjour") -> None, parse_mmss("3:60:00") -> None.

Erreur typée

Définir enum RacineErreur { Negatif } et fn isqrt(n: i64) -> Result<i64, RacineErreur> qui renvoie la partie entière de la racine carrée, ou Err(RacineErreur::Negatif) si n est négatif.

  • Objectif : type d'erreur dédié (enum) plutôt qu'une String.
  • Signature suggérée : fn isqrt(n: i64) -> Result<i64, RacineErreur>.
  • Cas de test : isqrt(16) -> Ok(4), isqrt(17) -> Ok(4), isqrt(-1) -> Err(RacineErreur::Negatif).

Chaîner avec ?

Écrire fn somme_deux(a: &str, b: &str) -> Result<i32, std::num::ParseIntError> qui parse deux chaînes en i32 et renvoie leur somme, en utilisant ? sur chaque parse.

  • Objectif : ? sur Result, propagation d'erreur, type d'erreur commun.
  • Signature suggérée : fn somme_deux(a: &str, b: &str) -> Result<i32, std::num::ParseIntError>.
  • Cas de test : somme_deux("2", "3") -> Ok(5), somme_deux("2", "x") -> Err(...).

Les closures

Ces exercices portent sur les fonctions anonymes, les fonctions d'ordre supérieur, la capture d'environnement et les traits Fn / FnMut. Faites-les dans l'ordre.

Appliquer n fois

Écrire une fonction qui applique n fois une fonction f à une valeur de départ : repeter(f, x, 3) vaut f(f(f(x))).

  • Objectif : paramètre F: Fn(i32) -> i32, boucle.
  • Signature suggérée : fn repeter<F: Fn(i32) -> i32>(f: F, x: i32, n: u32) -> i32.
  • Cas de test : repeter(|v| v + 1, 0, 5) -> 5, repeter(|v| v * 2, 1, 10) -> 1024, repeter(|v| v, 42, 0) -> 42.

Générateur (compteur)

Écrire une fonction qui renvoie une closure ; chaque appel de cette closure renvoie l'entier suivant, en commençant à 0.

  • Objectif : capture d'une variable mut, trait FnMut, impl FnMut() -> u32, move.
  • Signature suggérée : fn compteur() -> impl FnMut() -> u32.
  • Cas de test : let mut c = compteur(); puis c() -> 0, c() -> 1, c() -> 2.

Filtre paramétré

Écrire une fonction qui garde les éléments d'un slice satisfaisant un prédicat donné.

  • Objectif : impl Fn(&i32) -> bool en argument, construction d'un Vec.
  • Signature suggérée : fn garder(v: &[i32], pred: impl Fn(&i32) -> bool) -> Vec<i32>.
  • Cas de test : garder(&[1, 2, 3, 4, 5], |x| *x % 2 == 0) -> [2, 4], garder(&[1, 2, 3], |_| false) -> [].

Composer deux fonctions

Écrire composer(f, g) qui renvoie une closure x -> g(f(x)).

  • Objectif : deux paramètres génériques Fn, impl Fn(i32) -> i32 en retour, move.
  • Signature suggérée : fn composer<F, G>(f: F, g: G) -> impl Fn(i32) -> i32 where F: Fn(i32) -> i32, G: Fn(i32) -> i32.
  • Cas de test : let h = composer(|x| x + 1, |x| x * 3); h(2) -> 9.

Transformer une Option

À partir d'une Option<i32>, obtenir la valeur multipliée par 10 si elle existe, sinon -1, en une seule expression.

  • Objectif : Option::map, Option::unwrap_or.
  • Signature suggérée : fn dix_fois_ou_moins_un(o: Option<i32>) -> i32.
  • Cas de test : dix_fois_ou_moins_un(Some(4)) -> 40, dix_fois_ou_moins_un(None) -> -1.

Les itérateurs

Ces exercices portent sur iter / into_iter, map, filter, fold, zip, les consommateurs (sum, min, max, count...) et l'évaluation paresseuse. Faites-les dans l'ordre. Essayez de n'utiliser aucune boucle for.

Somme des carrés pairs

Calculer la somme des carrés des entiers pairs de 1 à n inclus, avec une chaîne d'itérateurs.

  • Objectif : (1..=n), filter, map, sum.
  • Signature suggérée : fn somme_carres_pairs(n: u32) -> u32.
  • Cas de test : somme_carres_pairs(10) -> 220 (4 + 16 + 36 + 64 + 100), somme_carres_pairs(1) -> 0.

Produit scalaire

Calculer le produit scalaire de deux slices de même longueur.

  • Objectif : zip, map, sum.
  • Signature suggérée : fn produit_scalaire(a: &[i64], b: &[i64]) -> i64.
  • Cas de test : produit_scalaire(&[1, 2, 3], &[4, 5, 6]) -> 32, produit_scalaire(&[], &[]) -> 0.

Mot le plus long

Renvoyer le mot le plus long d'une phrase. En cas d'égalité, le premier.

  • Objectif : split_whitespace, fold Option.
  • Signature suggérée : fn plus_long(phrase: &str) -> Option<&str>.
  • Cas de test : plus_long("le chat dort tranquillement") -> Some("tranquillement"), plus_long("") -> None, plus_long("a bb cc") -> Some("bb").

Réimplémenter count

Écrire une fonction qui compte les éléments d'un itérateur en utilisant uniquement fold (pas .count()).

  • Objectif : fold, accumulateur.
  • Signature suggérée : fn compte<I: Iterator>(it: I) -> usize.
  • Cas de test : compte(0..10) -> 10, compte(vec![1, 2, 3].into_iter()) -> 3, compte(std::iter::empty::<i32>()) -> 0.

Moyennes mobiles (fenêtre de 2)

À partir d'un slice de f64, renvoyer le vecteur des moyennes de chaque paire d'éléments consécutifs.

  • Objectif : slice::windows(2), map.
  • Signature suggérée : fn moyennes_paires(v: &[f64]) -> Vec<f64>.
  • Cas de test : moyennes_paires(&[1.0, 3.0, 5.0]) -> [2.0, 4.0], moyennes_paires(&[10.0]) -> [].

Les collections

Ces exercices portent sur Vec, String et les slices (&[T], &str). Faites-les dans l'ordre.

Renverser les mots

Renverser l'ordre des mots d'une phrase : "le chat dort" -> "dort chat le". Les mots sont séparés par des espaces simples.

  • Objectif : split_whitespace, rev, collect::<Vec<_>>(), join(" ").
  • Signature suggérée : fn renverser_mots(s: &str) -> String.
  • Cas de test : renverser_mots("le chat dort") -> "dort chat le", renverser_mots("bonjour") -> "bonjour".

Histogramme de lettres

Compter les occurrences de chaque lettre minuscule ASCII (a-z) dans une chaîne. Les autres caractères sont ignorés.

  • Objectif : chars, indexation d'un [u32; 26] via c as usize - 'a' as usize.
  • Signature suggérée : fn compter_lettres(s: &str) -> [u32; 26].
  • Cas de test : pour "abbccc", l'indice 0 (a) vaut 1, l'indice 1 (b) vaut 2, l'indice 2 (c) vaut 3 ; pour "Rust!", u, s, t valent 1 et le reste 0 (le R majuscule et le ! sont ignorés).

Fusion de deux vecteurs triés

À partir de deux slices déjà triés par ordre croissant, produire un Vec trié contenant tous leurs éléments.

  • Objectif : deux indices qui avancent en parallèle, Vec::push.
  • Signature suggérée : fn fusion(a: &[i32], b: &[i32]) -> Vec<i32>.
  • Cas de test : fusion(&[1, 3, 5], &[2, 4, 6]) -> [1, 2, 3, 4, 5, 6], fusion(&[], &[1, 2]) -> [1, 2], fusion(&[1, 1], &[1]) -> [1, 1, 1].

Palindrome

Dire si une chaîne se lit pareil dans les deux sens. Raisonne sur les caractères (chars), pas sur les octets, et ignore la casse.

  • Objectif : chars, rev, comparaison ; piège de l'indexation d'une String.
  • Signature suggérée : fn est_palindrome(s: &str) -> bool.
  • Cas de test : est_palindrome("kayak") -> true, est_palindrome("Été") -> true, est_palindrome("rust") -> false.

Sans doublons

Renvoyer les éléments d'un slice sans doublons, en préservant l'ordre de première apparition.

  • Objectif : Vec, contains, push.
  • Signature suggérée : fn sans_doublons(v: &[i32]) -> Vec<i32>.
  • Cas de test : sans_doublons(&[1, 2, 1, 3, 2, 4]) -> [1, 2, 3, 4], sans_doublons(&[]) -> [].

Lifetimes

Ces exercices portent sur les annotations de durée de vie, les fonctions qui renvoient une référence, l'élision et le pattern NewType. Le chapitre est vu en survol : exercices courts.

Le plus long

Renvoyer la plus longue de deux chaînes empruntées. Ici, il faut annoter la durée de vie.

  • Objectif : fn f<'a>(a: &'a str, b: &'a str) -> &'a str.
  • Signature suggérée : fn plus_long<'a>(a: &'a str, b: &'a str) -> &'a str.
  • Cas de test : plus_long("court", "beaucoup plus long") -> "beaucoup plus long", plus_long("ab", "cd") -> "ab".

Premier mot

Renvoyer le premier mot d'une chaîne (jusqu'au premier espace, ou toute la chaîne). Ici aucune annotation n'est nécessaire - explique pourquoi en commentaire.

  • Objectif : élision des durées de vie (une seule référence en entrée -> la sortie prend la même).
  • Signature suggérée : fn premier_mot(s: &str) -> &str.
  • Cas de test : premier_mot("bonjour le monde") -> "bonjour", premier_mot("salut") -> "salut", premier_mot("") -> "".

Struct qui emprunte

Définir struct Extrait<'a> { texte: &'a str } et une méthode mot(&self, i: usize) -> Option<&str> qui renvoie le i-ème mot.

  • Objectif : paramètre de durée de vie sur une struct, impl<'a>.
  • Signature suggérée : impl<'a> Extrait<'a> { fn mot(&self, i: usize) -> Option<&str> }.
  • Cas de test : Extrait { texte: "un deux trois" }.mot(1) -> Some("deux"), .mot(9) -> None.

Min d'un slice

Renvoyer une référence vers le plus petit élément d'un slice, ou None s'il est vide.

  • Objectif : fn f<'a>(v: &'a [i32]) -> Option<&'a i32>, ou .iter().min().
  • Signature suggérée : fn min_ref<'a>(v: &'a [i32]) -> Option<&'a i32>.
  • Cas de test : min_ref(&[4, 1, 7]) -> Some(&1), min_ref(&[]) -> None.

CLI

Ces exercices portent sur les arguments de ligne de commande (std::env::args), la crate clap et la lecture de fichiers. À faire dans un projet à part (cargo new). Faites-les dans l'ordre.

Écho des arguments

Écrire un programme qui affiche chacun de ses arguments sur une ligne, sans clap. Ignore le premier (le nom du binaire).

  • Objectif : std::env::args(), .skip(1), for.
  • Signature suggérée : fn main().
  • Cas de test : cargo run -- a b c affiche a, b, c (un par ligne).

Additionneur

cargo run -- 1 2 3 4 affiche 10. Chaque argument est un entier ; si l'un n'est pas un entier, afficher un message d'erreur et quitter avec un code de sortie non nul.

  • Objectif : args, parse, gestion d'erreur, std::process::exit.
  • Signature suggérée : fn main().
  • Cas de test : -- 1 2 3 4 -> 10 ; -- 1 x 3 -> message d'erreur, code de sortie non nul.

clap : salutation

Avec clap (#[derive(Parser)]), un CLI qui accepte --nom <NOM> (obligatoire) et --repeter <N> (optionnel, défaut 1), et affiche "Bonjour !" N fois.

  • Objectif : #[derive(Parser)], #[arg(short, long)], valeur par défaut, Option.
  • Signature suggérée : struct Args { nom: String, repeter: u32 }.
  • Cas de test : -- --nom Alice --repeter 3 affiche 3 lignes "Bonjour Alice !" ; -- --nom Bob en affiche 1 ; sans --nom -> message d'usage de clap.

Compteur de lignes

cargo run -- fichier.txt affiche le nombre de lignes du fichier. Message d'erreur lisible si le fichier n'existe pas.

  • Objectif : argument, std::fs::read_to_string, str::lines().count(), ?.
  • Signature suggérée : fn main() -> Result<(), Box<dyn std::error::Error>>.
  • Cas de test : sur un fichier de 3 lignes -> 3 ; sur un chemin inexistant -> message d'erreur, code de sortie non nul.

Unsafe

Ces exercices portent sur les pointeurs bruts (*const T, *mut T), les blocs et fonctions unsafe, et la liste chaînée. Le chapitre est vu en survol : exercices courts, avec un bloc unsafe minimal et commenté.

Échanger via pointeurs bruts

Échanger les valeurs pointées par deux *mut i32, dans un bloc unsafe.

  • Objectif : *mut T, déréférencement dans unsafe { ... }, std::ptr::swap ou lecture/écriture manuelle.
  • Signature suggérée : fn echanger(a: *mut i32, b: *mut i32).
  • Cas de test : let (mut x, mut y) = (1, 2); echanger(&mut x, &mut y); -> x == 2 && y == 1.

Somme d'un buffer

Calculer la somme de n entiers consécutifs à partir d'un pointeur *const i32.

  • Objectif : unsafe fn, arithmétique de pointeurs (p.add(i)), déréférencement.
  • Signature suggérée : unsafe fn somme(p: *const i32, n: usize) -> i32.
  • Cas de test : pour let v = [1, 2, 3, 4];, unsafe { somme(v.as_ptr(), 4) } -> 10.

len() sur la liste chaînée

Ajouter à la liste chaînée unsafe du chapitre une méthode len(&self) -> usize qui suit les pointeurs next jusqu'au pointeur nul.

  • Objectif : parcours de pointeurs bruts, while !p.is_null(), unsafe.
  • Signature suggérée : impl LinkedList { fn len(&self) -> usize }.
  • Cas de test : liste vide -> 0 ; après trois push -> 3.

peek()

Ajouter une méthode peek(&self) -> Option<i32> qui renvoie la valeur de tête sans la retirer.

  • Objectif : lire (*head).data en unsafe sans modifier la liste.
  • Signature suggérée : impl LinkedList { fn peek(&self) -> Option<i32> }.
  • Cas de test : après push(1); push(2);, peek() -> Some(2), et un second peek() renvoie encore Some(2) ; sur une liste vide -> None.

Exercices avancés

Les exercices que nous proposons ici sont très vagues. L'objectif est de vous donner des thèmes, de choses que vous connaissez du cours de programmation et d'algorithmique de l'année passée et de les implémenter un peu comme vous voulez sans contraintes particulières pour que vous expérimentiez avec le langage (oui vous êtes des grand·e·s maintenant).

Essayez au maximum d'utiliser les concepts de Rust vus en classe.

Vous trouverez une liste ci-dessous, avec un degré de difficulté croissant (plus ou moins). L'idée est d'avancer à votre rythme (ce qui n'est pas équivalent à regarder le plafond en attendant que ça passe) et vous donner des idées d'exercices devenant plus complexes au fur et à mesure de votre périple.

  • Le nombre secret: implémenter le jeu du nombre secret qui serait joué par des IA utilisant des stratégies différentes.
  • Calculer pi: avec la méthode des fléchettes.
  • Une pile simple: implémenter une pile utilisant un tableau statique comme structure de données.
  • Un tri: un tri à bulles, tri à deux piles et/ou un quicksort ça fait l'affaire.
  • Couverture de la reine: écrire un code qui permet de trouver pour une position de départ aléatoire toutes les solutions de couverture de la reine pour un tableau 8x8.
  • Les fractions: écrire une librairie de fractions en utilisant les traits Add, Sub, etc.
  • Les vecteurs: écrire une librairie de vecteurs en utilisant les traits Add, Sub, etc.
  • Le jeu du pendu: implémenter le jeu du pendu où les mots sont des chaînes de caractères aléatoires et les lettres tirées sont également tirées au hasard.
  • Compter différemment: écrire un programme qui permet de compter comme au tennis où chaque joueur gagne un point avec une probabilité donnée.
  • Le puissance 4: simulez le jeu du puissance 4.
  • Le jeu de la bataille: simulez le jeu de carte de la bataille entre deux machines.

Aller plus haut

  • Ascii art: écrire un convertisseur d'image en ASCII art.
  • Simulation du système solaire: écrire un simulateur de système solaire.