Skip to content
Snippets Groups Projects
Commit 5594196a authored by orestis.malaspin's avatar orestis.malaspin
Browse files

Merge branch 'part03summary' into 'main'

Part03summary

See merge request orestis.malaspin/rust-101!24
parents b99c8dcb ce93aa6e
No related branches found
No related tags found
No related merge requests found
...@@ -4,3 +4,6 @@ language = "fr" ...@@ -4,3 +4,6 @@ language = "fr"
multilingual = false multilingual = false
src = "src" src = "src"
title = "Rust-101: Université d'été" title = "Rust-101: Université d'été"
[output.html.playground]
editable = true
...@@ -3,3 +3,4 @@ ...@@ -3,3 +3,4 @@
- [Part 00](./part00.md) - [Part 00](./part00.md)
- [Part 01](./part01.md) - [Part 01](./part01.md)
- [Part 02](./part02.md) - [Part 02](./part02.md)
- [Part 03](./part03.md)
# Discussion du code `part03`
Dans cette partie nous discutons de [ce code](#le-code).
## Concepts
Les concepts abordés dans cet exemple sont:
1. [La généricité et les traits.](#la-généricité-et-les-traits)
2. [Le trait `Minimum`.](#le-trait-minimum)
3. [Les tuples](#les-tuples)
4. [Les traits `Clone` et `Copy`.](#les-traits-clone-et-copy)
## Discussion
### La généricité et les traits
En Rust (comme dans beaucoup de langages) on a un moyen d'éviter de dupliquer du code en
utilisant le concept de *généricité*. Il s'agit ici de remplacer un type par un caractère générique
lors de la définition d'un type ou d'une fonction.
Jusqu'ici nous avions une structure `NumberOrNothing` qui contenait soit `Nothing` soit `Number(i32)` qui encapsule un entier 32 bits. Afin d'éviter de devoir réécrire tout le code pour chaque
type (`u64`, `double`, ou n'importe quel autre type) on va réécrire notre type énuméré
en le renommant astucieusement `SomethingOrNothing` (en effet, il se pourrait que nous ne voulions
plus uniquement trouver le plus petit nombre dans une liste, mais on pourrait vouloir
trouver le mot le plus "petit" dans l'ordre lexicographique). Ainsi on a
```rust,no_run
{{#include ../../codes/rust_lang/part03/src/main.rs:something_or_nothing}}
```
Il faut noter ici la présence du caractère générique `T`, qui est déclaré comme générique
lors de la définition du type `SomethingOrNothing<T>`, puis est utilisé à la place de `i32`
dans `Something(T)`.
Maintenant qu'on a changé la définition de noter type, plus rien fonctionne et on doit adapter
le reste du code. Pour aller dans l'ordre, on doit modifier l'implémentation de la fonction `SomethingOrNothing::new(val)`
```rust,no_run
{{#include ../../codes/rust_lang/part03/src/main.rs:something_or_nothing}}
{{#include ../../codes/rust_lang/part03/src/main.rs:new}}
```
On voit ici qu'il faut annoter tout le bloc `impl<T> SomethingOrNothing<T>` avec le type générique
afin qu'il puisse être réutilisé dans les fonctions statiques. En effet,
si on omet les `<T>` on une erreur de compilation
```rust,compile_fail
{{#include ../../codes/rust_lang/part03/src/main.rs:something_or_nothing}}
impl SomethingOrNothing<T> {
fn new(val: T) -> SomethingOrNothing<T> {
SomethingOrNothing::Something(val)
}
}
```
Afin d'illustrer une particularité de la généricité de Rust, nous avons également réécrit la fonction
`print(val)`
```rust,no_run
{{#include ../../codes/rust_lang/part03/src/main.rs:something_or_nothing}}
{{#include ../../codes/rust_lang/part03/src/main.rs:print}}
```
On voit ici qu'il y a une annotation particulière dans l'entête de la fonction, `T: std::fmt::Display`
```rust,ignore
fn print<T: std::fmt::Display>(val: SomethingOrNothing<T>)
```
Cette syntaxe dit au compilateur que le type générique implémente une fonctionnalité
particulière, nommée **trait**, qui dit au programme comment afficher une variable du type
générique `T`. On voit qu'on doit afficher la valeur `val` encapsulée dans `Something(val)`
```rust,ignore
SomethingOrNothing::Something(val) => println!("Something is: {}", val),
```
Hors si on ne dit pas à notre programme comment faire cet affichage, il sera bien embêté. Ici,
nous devons donc préciser qu'il est **nécessaire** que `T` implémente le trait `Display`
sinon le programme ne compilera pas (cliquez sur play pour le vérifier)
```rust,compile_fail
{{#include ../../codes/rust_lang/part03/src/main.rs:something_or_nothing}}
fn print<T>(val: SomethingOrNothing<T>) {
match val {
SomethingOrNothing::Nothing => println!("Nothing."),
SomethingOrNothing::Something(val) => println!("Something is: {}", val),
}
}
```
On retrouve la contrainte d'implémenter le trait `Display` dans la fonction `print_tab(tab)`.
Corriger le code ci-dessous pour qu'il compile
```rust,compile_fail,editable
const SIZE: usize = 9;
fn read_command_line() -> [i32; SIZE] {
[10, 32, 12, 43, 52, 53, 83, 2, 9]
}
fn print_tab<T>(tab: [T; SIZE]) {
for t in tab {
print!("{} ", t);
}
println!();
}
fn main() {
print_tab(read_command_line());
}
```
### Le trait `Minimum`
La fonctionnalité principale dont nous avons besoin pour que notre code fonctionne est de pouvoir trouver
le minimum d'une liste de `SomethingOrNothing<T>` (voir l'appel à la fonction `current_minimum.min(SomethingOrNothing::new(t))`).
```rust,ignore
{{#include ../../codes/rust_lang/part03/src/main.rs:find_min}}
```
Ainsi on doit annoter `T` pour qu'il puisse calculer la plus petite valeur entre deux `SomethingOrNothing<T>`. On va donc devoir écrire définir notre premier *trait*.
On définit un trait à l'aide de la syntaxe suivante
```rust,no_run
{{#include ../../codes/rust_lang/part03/src/main.rs:minimum}}
```
Ici le trait `Minimum` sera implémenté sur un type qui implémente le trait `Copy` (un trait qui
garantit qu'on sait comment copier une valeur). Notre trait n'a que la fonction `min` (le nombre
de fonction dans un trait est arbitraire, il peut même être nul).
L'entête de la fonction nous dit que la fonction `min` a deux argument, la variable sur laquelle
elle est appelée, et une variable du même type (annoté avec le type `Self`, avec un "s"
majuscule, contrairement à `self` qui fait référence à une variable) et retourne également une
valeur du même type.
Il est important de vous rappeler qu'ici on ne sait pas encore quel est le type sur lequel on implémente cette fonction.
L'implémentation de `Minimum` pour `SomethingOrNothing<T>` se fait comme suit
```rust,ignore
{{#include ../../codes/rust_lang/part03/src/main.rs:minimum}}
{{#include ../../codes/rust_lang/part03/src/main.rs:minimum_impl}}
```
Pour implémenter un trait sur un type on utilise la syntaxe
```rust,ignore
impl Trait for Type
```
puis implémenter **toutes** les fonctions se trouvant dans la définition du trait.
Ici, nous n'avons que la fonction `min` à implémenter.
Le type `SomethingOrNothing` est générique, donc il faut à nouveau notre code
pour en tenir compte. On voit que le type générique doit lui-même
implémenter le trait `Minimum` pour que cette fonction compile: on voit l'utilisation
de la fonction `min`
```rust,ignore
SomethingOrNothing::new(lhs.min(rhs))
```
Nous verrons le détail de la syntaxe de cette fonction dans la section sur [les tuples](#les-tuples).
Comme nous utilisons des `SomethingOrNothing<i32>` dans ce code, nous devons implémenter
le trait `Minimum` pour des entiers. Ce qui est fait dans le bout de code suivant
```rust,no_run
{{#include ../../codes/rust_lang/part03/src/main.rs:minimum}}
{{#include ../../codes/rust_lang/part03/src/main.rs:minimum_i32}}
```
### Les tuples
On découvre ici, une syntaxe inconnue: les **tuples**. Un tuple est une collection
d'un nombre arbitraire de valeurs dont les types peuvent être différents.
Un tuple lui-même est une valeur dont le type est
```rust,ignore
(T1, T2, T3, ...)
```
`T1`, `T2`, `T3`, ... sont les types des membres du tuple. Un tuple peut être utilisé pour retourner plusieurs valeurs depuis une fonction par exemple.
Ainsi quand on fait du *pattern mathching*
```rust,ignore
match (self, rhs)
```
on va créer un tuple avec les valeurs de `self` et de `rhs` et vérifier les types de toutes les valeurs possibles pour ces types énumérées. On a donc 4 cas différents:
```rust,ignore
match (self, rhs) {
(SomethingOrNothing::Nothing, SomethingOrNothing::Nothing) => {
SomethingOrNothing::Nothing
}
(SomethingOrNothing::Something(lhs), SomethingOrNothing::Something(rhs)) => {
SomethingOrNothing::new(lhs.min(rhs))
}
(SomethingOrNothing::Nothing, SomethingOrNothing::Something(rhs)) => {
SomethingOrNothing::new(rhs)
}
(SomethingOrNothing::Something(lhs), SomethingOrNothing::Nothing) => {
SomethingOrNothing::new(lhs)
}
}
```
quand on a deux variantes `Nothing`, on retourne `Nothing`, quand on a une variante `Nothing`
et une valeur `Something(val)` on retourne `Something(val)` et finalement quand on a
`Something(lhs)` et un `Something(rhs)`, c'est `Something(lhs.min(rhs))` qui est retourné (donc
la valeur la plus petite qui encapsulée). D'où la nécessité que `T` implémente le trait `Minimum`.
Il faut noter à nouveau qu'il n'y a pas de `;` dans les blocs des variantes du pattern matching et que les valeurs sont retournées (la structure de contrôle `match` est une expression).
### Les traits `Clone` et `Copy`
En Rust, il existe deux traits essentiels `Copy` et `Clone`. Le trait `Copy` permet de copier les instances d'un type bit à bit. La copie est une action **implicite**. Par exemple, dans le code ci-dessous
```rust,ignore
let y : i32 = 5;
let x = y;
```
la valeur de `y` (ici `5`) est copiée dans une zone mémoire nouvellement allouée qui ensuite est liée à la variable `x`.
Dans notre code nous décidons d'autoriser la copie de notre type énuméré en implémentant le trait `Copy`
```rust,no_run
{{#rustdoc_include ../../codes/rust_lang/part03/src/main.rs:copy}}
```
Comme on peut le voir ici, il n'y a pas de fonction à implémenter avec `Copy`, ce trait permet uniquement d'effectuer une copie binaire des données.
Il est également important de noter qu'afin que notre type `SomethingOrNothing<T>` implémente le trait `Copy`, il est nécessaire que le type paramètrique `T` implémente lui aussi `Copy`. Ce qui veut dire plus simplement qu'un type complexe ne peut pas implémenter le trait `Copy` si les types qu'il contient ne sont pas copiable.
On peut aussi remarquer qu'il est possible d'indiquer la nécessité que les types implémentant un trait soit copiables. Par exemple
```rust,no_run
{{#rustdoc_include ../../codes/rust_lang/part03/src/main.rs:minimum}}
```
où la notation `trait Minimum: Copy` spécifie que `Copy` doit être implémenté quand on implémente `Minimum`.
Ici tous les types qui implémentent le trait `Minimum` doivent également implémenter `Copy`. C'est le cas par exemple du type `i32`
```rust,no_run
{{#rustdoc_include ../../codes/rust_lang/part03/src/main.rs:minimum_i32}}
```
Le deuxième trait que nous retrouvons dans ce code est le trait `Clone`. `Clone` est un supertrait de `Copy`, ce qui signifie qu'un type qui implémente `Copy` doit nécessairement implémenter `Clone`.
Le trait `Clone` permet de dupliquer explicitement une instance. En effet, pour cloner une instance, il faut appeler la méthode `clone()` explicitement
```rust,no_run
{{#rustdoc_include ../../codes/rust_lang/part03/src/main.rs:clone}}
```
Comme on peut le voir dans le code ci-dessus, il est possible de définir un comportement arbitraire en redéfinissant la méthode `clone()`.
En effet, le trait `Clone` nous permet de définir librement le comportement attendu lors d'un clonage, ce qui n'est pas le cas avec `Copy`.
Cette liberté a un coût, puisque que l'on peut écrire notre propre fonction de clonnage, ce dernier peut facilement devenir beaucoup plus coûteux que la simple copie binaire des données.
Le langage Rust offre un attribut afin de pouvoir implémenter simplement les traits `Copy` et `Clone`. Il s'agit de `#[derive(...)]`.
Par exemple avec le code suivant
```rust,no_run
#[derive(Clone,Copy)]
struct MyStruct;
```
l'annotation permet d'indiquer au compilateur que notre `struct MyStruct` nécessite une implémentation par défaut des traits `Copy` et `Clone`.
Ce qui est équivaut à écrire
```rust,no_run
struct MyStruct;
impl Copy for MyStruct { }
impl Clone for MyStruct {
fn clone(&self) -> MyStruct {
*self
}
}
```
## Le code
```rust
{{#rustdoc_include ../../codes/rust_lang/part03/src/main.rs:main}}
```
...@@ -4,7 +4,12 @@ make -C c_lang/min_list ...@@ -4,7 +4,12 @@ make -C c_lang/min_list
cd rust_lang cd rust_lang
for d in *; do for d in *; do
echo ==================== Running cargo run for $d ==================== echo ==================== Running cargo run for $d ====================
if [ $d == 'part07' ]
then
cargo run --manifest-path $d/Cargo.toml < $d/numbers.txt
else
cargo run --manifest-path $d/Cargo.toml cargo run --manifest-path $d/Cargo.toml
fi
echo ==================== Running cargo test for $d ==================== echo ==================== Running cargo test for $d ====================
cargo test --manifest-path $d/Cargo.toml --workspace --verbose cargo test --manifest-path $d/Cargo.toml --workspace --verbose
echo ==================== Running cargo doc for $d ==================== echo ==================== Running cargo doc for $d ====================
......
/* ANCHOR: all */
/// In part03 we introduce genericity through traits and in particular, [Copy], /// In part03 we introduce genericity through traits and in particular, [Copy],
/// [Clone], [std::fmt::Display] . /// [Clone], [std::fmt::Display] .
// ANCHOR: something_or_nothing
enum SomethingOrNothing<T> { enum SomethingOrNothing<T> {
Nothing, Nothing,
Something(T), Something(T),
} }
// ANCHOR_END: something_or_nothing
// ANCHOR: new
impl<T> SomethingOrNothing<T> {
fn new(val: T) -> SomethingOrNothing<T> {
SomethingOrNothing::Something(val)
}
}
// ANCHOR_END: new
// ANCHOR: print
// Print function
// We know the generic type T must be Displayable // We know the generic type T must be Displayable
impl<T: std::fmt::Display> SomethingOrNothing<T> { fn print<T: std::fmt::Display>(val: SomethingOrNothing<T>) {
// Static function match val {
fn print(self) {
match self {
SomethingOrNothing::Nothing => println!("Nothing."), SomethingOrNothing::Nothing => println!("Nothing."),
SomethingOrNothing::Something(val) => println!("Something is: {}", val), SomethingOrNothing::Something(val) => println!("Something is: {}", val),
} }
} }
} // ANCHOR_END: print
// ANCHOR: clone
impl<T: Clone> Clone for SomethingOrNothing<T> { impl<T: Clone> Clone for SomethingOrNothing<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
match self { match self {
SomethingOrNothing::Nothing => SomethingOrNothing::Nothing, SomethingOrNothing::Nothing => SomethingOrNothing::Nothing,
SomethingOrNothing::Something(val) => SomethingOrNothing::Something(val.clone()), SomethingOrNothing::Something(val) => SomethingOrNothing::new(val.clone()),
} }
} }
} }
// ANCHOR_END: clone
// ANCHOR: copy
impl<T: Copy> Copy for SomethingOrNothing<T> {} impl<T: Copy> Copy for SomethingOrNothing<T> {}
// ANCHOR_END: copy
// If we remove Copy, we have a problem with the t in tab // If we remove Copy, we have a problem with the t in tab
// in the computation of the minimum. // in the computation of the minimum.
// ANCHOR: minimum
trait Minimum: Copy { trait Minimum: Copy {
fn min(self, rhs: Self) -> Self; fn min(self, rhs: Self) -> Self;
} }
// ANCHOR_END: minimum
// ANCHOR: minimum_impl
impl<T: Minimum> Minimum for SomethingOrNothing<T> { impl<T: Minimum> Minimum for SomethingOrNothing<T> {
fn min(self, rhs: Self) -> Self { fn min(self, rhs: Self) -> Self {
match (self, rhs) { match (self, rhs) {
...@@ -41,20 +59,22 @@ impl<T: Minimum> Minimum for SomethingOrNothing<T> { ...@@ -41,20 +59,22 @@ impl<T: Minimum> Minimum for SomethingOrNothing<T> {
SomethingOrNothing::Nothing SomethingOrNothing::Nothing
} }
(SomethingOrNothing::Something(lhs), SomethingOrNothing::Something(rhs)) => { (SomethingOrNothing::Something(lhs), SomethingOrNothing::Something(rhs)) => {
SomethingOrNothing::Something(lhs.min(rhs)) SomethingOrNothing::new(lhs.min(rhs))
} }
(SomethingOrNothing::Nothing, SomethingOrNothing::Something(rhs)) => { (SomethingOrNothing::Nothing, SomethingOrNothing::Something(rhs)) => {
SomethingOrNothing::Something(rhs) SomethingOrNothing::new(rhs)
} }
(SomethingOrNothing::Something(lhs), SomethingOrNothing::Nothing) => { (SomethingOrNothing::Something(lhs), SomethingOrNothing::Nothing) => {
SomethingOrNothing::Something(lhs) SomethingOrNothing::new(lhs)
} }
} }
} }
} }
// ANCHOR_END: minimum_impl
// i32 is Copyable as a very basic type as f32, f64, etc. // i32 is Copyable as a very basic type as f32, f64, etc.
// Arrays for example are not copyable. // Arrays for example are not copyable.
// ANCHOR: minimum_i32
impl Minimum for i32 { impl Minimum for i32 {
fn min(self, rhs: Self) -> Self { fn min(self, rhs: Self) -> Self {
if self < rhs { if self < rhs {
...@@ -64,6 +84,7 @@ impl Minimum for i32 { ...@@ -64,6 +84,7 @@ impl Minimum for i32 {
} }
} }
} }
// ANCHOR_END: minimum_i32
const SIZE: usize = 9; const SIZE: usize = 9;
...@@ -73,31 +94,34 @@ fn read_command_line() -> [i32; SIZE] { ...@@ -73,31 +94,34 @@ fn read_command_line() -> [i32; SIZE] {
// Prints tab and returns tab. // Prints tab and returns tab.
// Tab would be destructed at the end of the function otherwise. // Tab would be destructed at the end of the function otherwise.
fn print_tab(tab: [i32; SIZE]) -> [i32; SIZE] { // ANCHOR: print_tab
fn print_tab<T: std::fmt::Display>(tab: [T; SIZE]) {
for t in tab { for t in tab {
print!("{} ", t); print!("{} ", t);
} }
println!(); println!();
tab
} }
// ANCHOR_END: print_tab
fn find_min<T: Minimum>(tab: [T; SIZE]) -> ([T; SIZE], SomethingOrNothing<T>) { // ANCHOR: find_min
let mut min = SomethingOrNothing::Nothing; fn find_min<T: Minimum>(tab: [T; SIZE]) -> SomethingOrNothing<T> {
let mut current_minimum = SomethingOrNothing::Nothing;
// Here is T is not Copyable tab is consumed and cannot be returned // Here is T is not Copyable tab is consumed and cannot be returned
for t in tab { for t in tab {
min = min.min(SomethingOrNothing::Something(t)); current_minimum = current_minimum.min(SomethingOrNothing::new(t));
} }
(tab, min) current_minimum
} }
// ANCHOR_END: find_min
// ANCHOR: main
fn main() { fn main() {
let tab = read_command_line(); let tab = read_command_line();
println!("Among the Somethings in the list:"); println!("Among the Somethings in the list:");
let tab = print_tab(tab); print_tab(tab);
let min = find_min(tab);
// There are alternatives to access fields of tuples print(min);
let (_, min) = find_min(tab);
// The first field is not used therefore we can replace it with "_"
min.print();
} }
// ANCHOR_END: main
/* ANCHOR_END: all */
3478
26
-127
1287
189
\ No newline at end of file
...@@ -49,7 +49,7 @@ pub fn print_tab(tab: &Vec<i32>) { ...@@ -49,7 +49,7 @@ pub fn print_tab(tab: &Vec<i32>) {
/// Prints all the elements of the `tab`. /// Prints all the elements of the `tab`.
/// Tab is borrowed here /// Tab is borrowed here
pub fn print_tab_CustomInt(tab: &Vec<CustomInt>) { pub fn print_tab_custom_int(tab: &Vec<CustomInt>) {
for i in tab { for i in tab {
println!("{i} "); println!("{i} ");
} }
......
...@@ -7,7 +7,7 @@ fn main() -> Result<(), String> { ...@@ -7,7 +7,7 @@ fn main() -> Result<(), String> {
Err(s) => return Err(s), Err(s) => return Err(s),
}; };
println!("Among the custom ints in the list:"); println!("Among the custom ints in the list:");
io::print_tab_CustomInt(&tab); io::print_tab_custom_int(&tab);
// There are alternatives to access fields of tuples // There are alternatives to access fields of tuples
let min = find_min(&tab); let min = find_min(&tab);
// The first field is not used therefore we can replace it with "_" // The first field is not used therefore we can replace it with "_"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment