Pour commencer nous allons nous familiariser avec la notion de propriété (Ownership en anglais) et le système de vérification d’emprunts mémoire de Rust (Borrow Checker en anglais). Pour cela nous allons faire les exercices suivants:
Vous pouvez répondre aux exercices directement dans votre navigateur.
Dans ce TP nous allons implémenter un arbre binaire de recherche (ABR) en Rust.
L’objectif est de bien comprendre le système de vérification d’emprunts mémoire. L’implémentation d’un ABR est particulièrement adapté car elle nécessite de manipuler les nombreuses références vers les nœuds de l’arbre.
Les nœuds d’un ABR possèdent chacun une valeur associée unique (deux nœuds ne peuvent pas avoir la même valeur). Il existe un ordre total sur l’ensemble des valeurs.
Pour un nœud d’un ABR de valeur x, toutes les valeurs du sous-arbre gauche sont plus petites que x et toutes les valeurs du sous-arbre droit sont plus grandes que x.
Un ABR permet de déterminer si une valeur est présente avec une complexité logarithmique.
Une première tentative pour représenter un nœud en Rust est
pub struct Node {
value: i32,
left: Node,
right: Node
}
-
Essayez de compiler le type suivant avec Rust. Expliquez l’erreur obtenue.
Dans l’erreur retournée, le compilateur nous suggère d’introduire une
indirection. Les champs left
et right
vont contenir des références
indirectes vers d’autres nœuds.
Parce que nous souhaitons allouer l’ABR sur le tas, nous allons utiliser une référence Box<>
en introduisant un type Box<Node>
.
pub struct Node {
value: i32,
left: Box<Node>,
right: Box<Node>
}
Ce nouveau code compile ! Malheureusement il ne nous permet pas de représenter des arbres vides. Cela est indispensable, en effet les feuilles de l’arbre possèdent des sous-arbre gauche et droit vides.
Dans un langage comme C ou C++, nous pourrions utiliser un pointeur nul pour représenter le cas de l’arbre vide. Néanmoins dans Rust les références nulles sont interdites pour éviter des erreurs à l’exécution. À la place Rust préconise l’utilisation d’un type Option<>
qui représente une valeur optionnelle.
Notre structure devient donc
pub struct Node {
value: i32,
left: Option<Box<Node>>,
right: Option<Box<Node>>
}
et de manière à permettre de manipuler un arbre vide directement, nous allons la réécrire de la manière suivante,
#[derive(Debug)]
pub struct Tree(Option<Box<Node>>);
#[derive(Debug)]
pub struct Node {
value: i32,
left: Tree,
right: Tree,
}
Les macros [derive(Debug)]
permettent d’obtenir des routines d’affichage
pour le debug automatiquement. En les utilisant il est possible d’afficher un objet arbre avec la syntaxe println!("{:?}", mon_arbre)
.
Pour représenter un arbre composé d’un unique nœud racine avec la valeur 12 nous écririons,
let t = Tree(Some(Box::new(Node {
value: 12,
left: Tree(None),
right: Tree(None),
})));
println!("{:#?}", t);
-
Modifiez l’exemple précédent pour rajouter un fils gauche (8) et un fils droit (27). Vérifiez que l’arbre s’affiche correctement.
let t = Tree(Some(Box::new(Node {
value: 12,
left: Tree(Some(Box::new(Node{value: 8, left: Tree(None), right: Tree(None) }))),
right: Tree(Some(Box::new(Node{value: 27, left: Tree(None), right: Tree(None) }))),
})));
println!("{:#?}", t);
Comme nous venons de le voir, il est pour l’instant fastidieux de manipuler nos structures. Pour faciliter l’utilisation nous allons rajouter quelques constructeurs.
-
Implémentez le constructeur
pub fn new() → Self
pour la structureTree
, cette méthode retournera un arbre vide. -
Implémentez le constructeur
pub fn leaf(value: i32) → Self
qui retournera un nœud de valeurvalue
sans enfants. -
Rajoutez des tests pour ces deux méthodes.
Pour tester si une valeur appartient à l’ABR il suffit de parcourir l’arbre en partant de la racine. Pour chaque nœud l’on compare la valeur cherchée à la valeur courante. Si elle sont identiques alors nous retournons true
. Si ce n’est pas le cas nous récursivons soit sur l’arbre gauche lorsque la valeur est inférieure ou l’arbre droit lorsque la valeur est supérieure.
-
Implémentez dans
Tree
la méthodepub fn contains(&self, value: i32) → bool
qui retourne vrai si et seulement sivalue
est présente dans un des nœuds de l’arbre.
Pour rajouter une nouvelle valeur à l’ABR on utilise une méthode similaire. Nous parcourons l’arbre: - si la racine est vide, nous le remplaçons par une feuille avec la nouvelle valeur; - si la racine est pleine, nous vérifions que la valeur n’est pas déjà présente (en effet il n’y a pas de doublons dans un ABR) puis nous récursivons sur le sous-arbre gauche ou le sous-arbre droit de manière à préserver l’invariant d’ordre.
-
Implémentez la méthode
pub fn insert(&mut self, value: i32) → bool
qui insèrevalue
dans l’ABR. La méthode retournetrue
lorsque l’insertion est possible etfalse
lorsque la valeur est déjà présente dans l’arbre. -
Rajoutez des tests unitaires pour les méthodes
contains
etinsert
.
La supression d’une valeur est un peu plus compliquée. Une méthode de suppression efficace est expliquée sur l’article français de Wikipedia.
-
Implémentez la méthode
pub fn delete(&mut self, value: i32)
en suivant l’algorithme précédent. La méthode retournetrue
lorsque la suppression est possible et retournefalse
lorsque la valeur n’est pas trouvée dans l’ABR.
-
Pour l’instant notre ABR utilise des entiers signés 32 bits (
i32
); néanmoins il est facile en utilisant un type générique de l’étendre à tout type possédant un ordre total. Pour spécifier que votre type générique possède un ordre total vous pouvez utiliser le traitOrd
. -
Plutôt que de retourner un booléen pour vérifier que les opérations
insert
etdelete
se sont bien déroulées il est aussi possible de retourner une erreur à l’aide d’un objet de typeResult<>
. Cela vous permettra d’explorer un autre mécanisme de gestion d’erreurs dans Rust.
-
Illustation d’arbre binaire dans le domaine publique.
-
TP similaire dans le cours CIS198 de U. Pennsylvania, https://github.com/cis198-2016s/homework/tree/master/hw02
-
Learn Rust with entirely too many linked lists, https://rust-unofficial.github.io/too-many-lists/. Un excellent tutoriel qui montre pas à pas comment implémenter des listes chaînées dans Rust et les problèmes que l’on peut rencontrer.