Skip to content
Snippets Groups Projects
Commit 55cb56b6 authored by Michaël El Kharroubi's avatar Michaël El Kharroubi :satellite:
Browse files

Merge branch 'part02summary' into 'main'

Summary of part 2 is now done

See merge request orestis.malaspin/rust-101!22
parents 768cde92 027baa1d
Branches
No related tags found
No related merge requests found
...@@ -2,3 +2,4 @@ ...@@ -2,3 +2,4 @@
- [Part 00](./part00.md) - [Part 00](./part00.md)
- [Part 01](./part01.md) - [Part 01](./part01.md)
- [Part 02](./part02.md)
# Discussion du code `part02`
## Concepts
Les concepts abordés dans cet exemple sont:
1. [Les types énumérés.](#les-types-énumérés)
2. [Les fonctions statiques.](#les-fonctions-statiques)
3. [Les arguments des fonctions en Rust.](#les-arguments-des-fonctions-en-rust)
4. [Le pattern matching.](#le-pattern-matching)
## Discussion
### Les types énumérés
Dans ce code, nous introduisons un type complexe: le type énuméré `NumberOrNothing`
```rust,no_run
enum NumberOrNothing {
Nothing,
Number(i32),
}
```
Un type énuméré est très utile quand on veut décrire un objet ayant un nombre fini de valeurs possibles.
Ici nous avons deux **variantes** pour le type `NumberOrNothing`:
1. `NumberOrNothing::Nothing` qui décrit l’absence de valeur,
2. `NumberOrNothing::Number(i32)` qui décrit la présence d'une valeur et qui encapsule un entier 32 bits.
On note ici, que pour accéder à une variante de `NumberOfNothing` il est nécessaire d'utiliser la syntaxe `Type::Variante`.
### Les fonctions statiques
Sur ce type énuméré, nous définissons deux **fonctions statiques** à l'aide de la syntaxe
```rust,ignore
impl NumberOrNothing {
// fonction statiques
}
```
Ici nous avons deux fonctions. La première, `fn new(val)`, créer une instance de `NumberOrNothing` à partir d'un entier et le stocke dans la variante `NumberOrNothing::Number(val)`. En général la fonction `new` sur un type est une sorte de "constructeur": elle permet de créer une nouvelle instance d'un type. La notion d'instance est très importante. Si nous prenons un type connu `i32`, il s'agit du type et `let i:i32 = 5;` est une instance de ce type.
```rust,no_run
# enum NumberOrNothing {
# Nothing,
# Number(i32),
# }
impl NumberOrNothing {
fn new(val: i32) -> Self {
NumberOrNothing::Number(val)
}
}
# fn main() {
let num = NumberOrNothing::new(10);
# }
```
On voit ici que pour appeler la fonction `new` on la préfixe par le nom du type
suivi de `::`, soit `NumberOrNothing::new(val)`.
La seconde fonction, `fn print(self)`, sert à afficher le contenu d'une instance d'un `NumberOrNothing`.
```rust
# enum NumberOrNothing {
# Nothing,
# Number(i32),
# }
impl NumberOrNothing {
# fn new(val: i32) -> Self {
# NumberOrNothing::Number(val)
# }
fn print(self) {
match self {
NumberOrNothing::Nothing => println!("No number."),
NumberOrNothing::Number(val) => println!("The number is: {}", val),
}
}
}
# fn main() {
let num = NumberOrNothing::new(10);
num.print();
// NumberOrNothing::print(num); équivalent
# }
```
### Les arguments des fonctions en Rust
Il y a plusieurs choses à noter dans cette fonction. La première est qu'elle prend en argument le mot-clé `self` qui se réfère à l'instance sur laquelle la fonction est appelée. Ainsi quand on veut appeler la fonction `print()` on utilise la syntaxe du sélecteur sur la variable sur laquelle s'appelle la fonction: `num.print()`. Il existe une syntaxe équivalente qui serait:
```rust,ignore
NumberOrNothing::print(num);
```
Avant de nous intéresser au code qui est présent dans le corps de la fonction `print()`, essayons d'appeler deux fois de suite la fonction `print()`:
```rust,compile_fail
# enum NumberOrNothing {
# Nothing,
# Number(i32),
# }
# impl NumberOrNothing {
# fn new(val: i32) -> Self {
# NumberOrNothing::Number(val)
# }
# fn print(self) {
# match self {
# NumberOrNothing::Nothing => println!("No number."),
# NumberOrNothing::Number(val) => println!("The number is: {}", val),
# }
# }
# }
fn main() {
let num = NumberOrNothing::new(10);
NumberOrNothing::print(num);
NumberOrNothing::print(num);
}
```
On constate que la compilation échoue avec un message intéressant
```ignore
Compiling playground v0.0.1 (/playground)
error[E0382]: use of moved value: `num`
--> src/main.rs:19:28
|
17 | let num = NumberOrNothing::new(10);
| --- move occurs because `num` has type `NumberOrNothing`, which does not implement the `Copy` trait
18 | NumberOrNothing::print(num);
| --------------------------- `num` moved due to this method call
19 | NumberOrNothing::print(num);
| ^^^ value used here after move
|
note: consider changing this parameter type in method `print` to borrow instead if owning the value isn't necessary
--> src/main.rs:9:14
|
9 | fn print(self) {
| ----- ^^^^ this parameter takes ownership of the value
| |
| in this method
note: `NumberOrNothing::print` takes ownership of the receiver `self`, which moves `num`
--> src/main.rs:9:14
|
9 | fn print(self) {
| ^^^^
For more information about this error, try `rustc --explain E0382`.
```
En fait, cela signifie que la fonction est devenue propriétaire de la valeur de `num` lors du premier appel à `print()`, puis lorsque la fonction s'est terminée, la variable et les données qu'elle contient a été détruite automatiquement. Ainsi la variable `num`
ne contient "plus rien" (la variable n'est plus liée à la valeur `Number(10)`) et ne peut pas rappeler `print()`. En fait, il s'agit du comportement par défaut des fonctions en Rust. Elles deviennent *propriétaires* (owners) des
valeurs passées en argument et que la valeur est *déplacée* (moved).
Mais alors comment cela se fait que les fonctions
```rust,ignore
print_tab(tab);
let min = find_min(tab);
```
ne donnent pas de message d'erreur à la compilation?
En fait, ici `tab` est copié implicitement avant d'être passé à ces deux fonctions. Ainsi, c'est cette valeur copiée qui est déplacée dans la fonction et qui est détruite à la fin.
Cela est le cas pour des types "simples" comme tous les types de base de Rust et les tableaux statiques si le type des éléments est également un type "copiable" (on dit qu'ils sont `Copy`). Ainsi, les types `i32`, `usize` et `[N; i32]` utilisés dans ce programme sont `Copy`. On verra dans les chapitres suivants plus de détails sur ce fonctionnement.
### Le pattern matching
L'autre nouveauté introduite dans ce code est le **pattern matching**.
```rust,ignore
match self {
NumberOrNothing::Nothing => println!("No number."),
NumberOrNothing::Number(val) => println!("The number is: {}", val),
}
```
Ici, on vérifie quelle variante du type est encapsulé par notre instance représentée par la variable `self`. Dans le cas où
c'est `NumberOrNothing::Nothing`, on affiche `No number.`, et dans le cas où c'est
`NumberOrNothing::Number(val)` on lie la valeur encapsulée dans la variante `Number`
et on affiche cette valeur. Le compilateur détectera s'il manque une variante
du type énuméré et produira une erreur si cela est le cas.
La syntaxe en général est a suivante
```rust,ignore
match variable {
variante_1 => {
expression
},
variante_2 => {
expression
},
// ...
variante_n => {
expression
},
_ => {
expression
},
}
```
où le dernier `_` est un remplacement pour toutes les variantes pas traitées du
type énuméré (il est similaire au `default` du `switch ... case` de C).
```rust
# enum NumberOrNothing {
# Nothing,
# Number(i32),
# }
# impl NumberOrNothing {
# fn new(val: i32) -> Self {
# NumberOrNothing::Number(val)
# }
# fn print(self) {
# match self {
# NumberOrNothing::Nothing => println!("No number."),
# NumberOrNothing::Number(val) => println!("The number is: {}", val),
# }
# }
# }
# const SIZE: usize = 9;
# fn read_command_line() -> [i32; SIZE] {
# [10, 32, 12, 43, 52, 53, 83, 2, 9]
# }
# fn print_tab(tab: [i32; SIZE]) {
# for t in tab {
# print!("{} ", t);
# }
# println!();
# }
# fn min_i32(lhs: i32, rhs: i32) -> i32 {
# if lhs < rhs {
# lhs
# } else {
# rhs
# }
# }
# fn find_min(tab: [i32; SIZE]) -> NumberOrNothing {
# let mut min = NumberOrNothing::Nothing;
# for t in tab {
# match min {
# NumberOrNothing::Nothing => min = NumberOrNothing::new(t),
# NumberOrNothing::Number(val) => min = NumberOrNothing::new(min_i32(val, t)),
# }
# }
# min
# }
fn main() {
let tab = read_command_line();
println!("Among the numbers in the list:");
print_tab(tab);
let min = find_min(tab);
min.print();
let nothing = NumberOrNothing::Nothing;
NumberOrNothing::print(nothing);
}
```
\ No newline at end of file
...@@ -28,12 +28,11 @@ fn read_command_line() -> [i32; SIZE] { ...@@ -28,12 +28,11 @@ 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] { fn print_tab(tab: [i32; SIZE]) {
for t in tab { for t in tab {
print!("{} ", t); print!("{} ", t);
} }
println!(); println!();
tab
} }
fn min_i32(lhs: i32, rhs: i32) -> i32 { fn min_i32(lhs: i32, rhs: i32) -> i32 {
...@@ -44,7 +43,7 @@ fn min_i32(lhs: i32, rhs: i32) -> i32 { ...@@ -44,7 +43,7 @@ fn min_i32(lhs: i32, rhs: i32) -> i32 {
} }
} }
fn find_min(tab: [i32; SIZE]) -> ([i32; SIZE], NumberOrNothing) { fn find_min(tab: [i32; SIZE]) -> NumberOrNothing {
let mut min = NumberOrNothing::Nothing; let mut min = NumberOrNothing::Nothing;
for t in tab { for t in tab {
match min { match min {
...@@ -52,17 +51,13 @@ fn find_min(tab: [i32; SIZE]) -> ([i32; SIZE], NumberOrNothing) { ...@@ -52,17 +51,13 @@ fn find_min(tab: [i32; SIZE]) -> ([i32; SIZE], NumberOrNothing) {
NumberOrNothing::Number(val) => min = NumberOrNothing::new(min_i32(val, t)), NumberOrNothing::Number(val) => min = NumberOrNothing::new(min_i32(val, t)),
} }
} }
(tab, min) min
} }
fn main() { fn main() {
let tab = read_command_line(); let tab = read_command_line();
println!("Among the numbers in the list:"); println!("Among the numbers in the list:");
let tab = print_tab(tab); print_tab(tab);
let min = find_min(tab);
// There are alternatives to access fields of tuples
let (_, min) = find_min(tab);
// The first field is not used therefore we can replace it with "_"
min.print(); min.print();
} }
use crate::minimum::Minimum;
/// Larger ints based on a [Vec] of [u8] to repensent arbitrary lengthy numbers.
/// The number has a sign as well.
pub struct BigInt {
/// The data contains the unsigned integers that are read from right to left
/// The number 1337 is stored as vec![7, 3, 3, 1]. Each number must be in the range [0,9]
/// and no trailing 0s are allowed.
data: Vec<u8>,
/// Contains the sign of the number +/-1;
sign: i8,
}
impl BigInt {
/// Tries to create a new [BigInt]. If the number is valid it returns
/// an Ok(BigInt) an Error otherwise.
///
/// # Examples
///
/// ```
/// use part08::big_int::BigInt;
/// let num = BigInt::try_new(vec![1, 2, 3, 4], 1);
/// assert!(num.is_ok());
/// let num = BigInt::try_new(vec![1, 2, 3, 4], -1);
/// assert!(num.is_ok());
/// let num = BigInt::try_new(vec![1, 2, 3, 4], 10);
/// assert!(num.is_err());
/// let num = BigInt::try_new(vec![1, 2, 3, 4], -10);
/// assert!(num.is_err());
/// ```
///
pub fn try_new(data: Vec<u8>, sign: i8) -> Result<Self, String> {
// EXERCISE:
// We don't check for trailing 0s but maybe this could be an exercise.
// Also we should check numbers are between 0 and 9.
if sign == 1 || sign == -1 {
Ok(BigInt { data, sign })
} else {
Err(String::from("Invalid sign."))
}
}
}
impl Clone for BigInt {
fn clone(&self) -> Self {
BigInt {
data: self.data.clone(),
sign: self.sign,
}
}
}
impl Minimum for BigInt {
// EXERCISE: Correct this function by using clippy and let it guide you.
// Get inspiration from Display to compute the Minimum
fn min(self, rhs: Self) -> Self {
if self.sign < rhs.sign {
return self;
} else if self.sign > rhs.sign {
return rhs;
}
if self.data.len() < rhs.data.len() {
return self;
} else if self.data.len() > rhs.data.len() {
return rhs;
}
for (l, r) in self.data.iter().rev().zip(rhs.data.iter().rev()) {
let ls = (*l as i8) * self.sign;
let rs = (*r as i8) * self.sign;
if ls < rs {
return self;
} else if ls > rs {
return rhs;
}
}
self
}
}
impl PartialEq for BigInt {
fn eq(&self, other: &Self) -> bool {
if self.sign == other.sign && self.data.len() == other.data.len() {
self.data
.iter()
.zip(other.data.iter())
.try_fold(true, |_, (l, r)| if l == r { Some(true) } else { None })
.is_some()
} else {
false
}
}
}
impl std::fmt::Display for BigInt {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// This could be replaced by an `?`
if self.sign == -1 {
if let Err(e) = write!(f, "-") {
return Err(e);
}
}
// This could be replaced by an `?`
let res = self
.data
.iter()
.rev()
.try_fold((), |_, t| write!(f, "{}", t));
res
}
}
// EXERCISE: write tests
// EXERCISE: Modify Minimum trait to take references?
#[cfg(test)]
mod tests {}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment