Discussion du code part01

Concepts

Les concepts abordés dans cet exemple sont:

  1. Les fonctions.
  2. Les if ... else sont des expressions.
  3. Itérer sur les éléments d'un tableaux directement avec la boucle for.

Discussion

Les fonctions

Dans cette seconde itération de notre programme calculant le minimum d'une liste de nombres, nous introduisons les fonctions en Rust.

La syntaxe d'une fonction est la suivante:

fn function_name(arg1: type, arg2: type, ...) -> ReturnType

Une fonction est annotée avec le mot-clé fn suivi de son identifiant. Son type de retour se trouve à droite après la flèche (en ASCII-art) ->. Si une fonction n'a pas de type de retour explicite (il n'y a pas de flèche) le type de retour est () qui rappelle le type void de C (aka sans type, mais oui c'est un type en Rust). Entre parenthèses se trouvent les arguments de la fonction suivi de leur type. Le nombre d'arguments est supérieur ou égal à zéro.

Une fonction est ensuite appelée par son identifiant et ses éventuels arguments.

const SIZE: usize = 9;
fn read_command_line() -> [i32; SIZE] {
    [10, 32, 12, 43, 52, 53, 83, 2, 9]
    // équivalent à return [10, 32, 12, 43, 52, 53, 83, 2, 9];
}
fn main() {
    read_command_line();
}

La fonction read_command_line() ne prend ainsi aucun argument et retourne un tableau statique d'entier 32 bits et de taille SIZE. Pour qu'une fonction retourne une valeur, on a deux solutions:

  1. Si la valeur est la dernière expression de la fonction, on peut juste mettre la valeur (ou la variable) sans point virgule à la fin de la ligne.
  2. On peut, de façon similaire au C, utiliser le mot clé return val; On constate ici que les tableaux statiques (contrairement à ce qui se passe dans le langage C) peuvent être retournés par une fonction (le tableau n'est pas désalloué au moment du retour).

La vérification de la taille du tableau se fait dans la fonction check_size(size: usize) qui prend donc un argument de type usize et ne retourne rien.

fn check_size(size: usize) {
    if size == 0 {
        panic!("Size is of tab = 0.");
    }
}
fn main() {
check_size(10); // runs alright
check_size(0);  // panics
}

À l'inverse la fonction print_tab(tab) prend en argument un tableau statique et ne retourne rien.

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 main() {
let tab = read_command_line();
print_tab(tab);
}

Les fonctions min_i32(lhs, rhs) retourne la plus petite valeur de deux entiers signés de 32 bits.

fn min_i32(lhs: i32, rhs: i32) -> i32 {
    if lhs < rhs {
        lhs
    } else {
        rhs
    }
}
fn main() {
let min = min_i32(2, 10);
println!("{min}");    
}

En Rust le if est une expression

Mais si vous étudiez attentivement ce code, on constate que le corps de la fonction est étrange. On voit qu'aucune ligne ne possède de point terminal. Cela voudrait dire que lhs ou rhs sont la dernière expression de la fonction. En fait ce qui se passe c'est que le if ... else est une expression. De façon similaire au if ternaire en C, le if ... else en Rust retourne une valeur. On peut écrire de façon équivalente au code ci-dessus

fn min_i32(lhs: i32, rhs: i32) -> i32 {
    let min = if lhs < rhs {
        lhs
    } else {
        rhs
    };
    min
}
fn main() {
let min = min_i32(2, 10);
println!("{min}");    
}

On voit qu'ici à la fin du bloc du else nous avons un point virgule qui indique la fin de la ligne. Ainsi lhs ou rhs sont liés à la variable immutable min.

Cette notion d'expression est très importante. Comme nous l'avons expliqué précédemment la dernière expression d'une fonction est évaluée puis retournée par cette dernière.

Un bloc de code qui produit un résultat est une expression.

En Rust, plusieurs structures de contrôle tel que le If sont des expression. Il n'est pas nécessaire de connaître la valeur de l'expression pour la retourner.

Nous pourrions considérer par exemple les fonctions suivante :

#![allow(unused)]
fn main() {
fn add(lhs: i32, rhs: i32) -> i32 {
    lhs + rhs
}

fn add_one(val: i32) -> i32 {
    add(1, val)
}
}

La boucle for revisitée

La dernière fonction à discuter est find_min(tab) qui retourne la valeur minimale se trouvant dans tab.

const SIZE: usize = 9;
fn read_command_line() -> [i32; SIZE] {
    [10, 32, 12, 43, 52, 53, 83, 2, 9]
}
fn check_size(size: usize) {
    if size == 0 {
        panic!("Size is of tab = 0.");
    }
}
fn min_i32(lhs: i32, rhs: i32) -> i32 {
    if lhs < rhs {
        lhs
    } else {
        rhs
    }
}
fn find_min(tab: [i32; SIZE]) -> i32 {
    check_size(SIZE);
    let mut min = i32::MAX;
    for t in tab {
        min = min_i32(min, t);
    }
    min
}
fn main() {
let min = find_min(read_command_line());
println!("The minimal value is: {min}");
}

Cette version du calcul du minimum contient une différence importante dans la syntaxe de la boucle for. En effet, ici on itère pas sur un indice et on accède ensuite aux éléments du tableau. La syntaxe

for val in tab

permet d'itérer directement sur les valeurs contenues dans tab. Au début de chaque tour de la boucle for, val prend la valeur "courante" du tableau. A la fin du bloc du for, la valeur courante prend la valeur "suivante" dans le tableau si elle existe. En fait le tableau est implicitement converti en itérateur (plus de détails plus tard sur le sujet).

Dans d'autres langages on appelle ça une boucle "for each" qui traduit en français signifie pour chaque.

Le code ci-dessus est strictement équivalent à

const SIZE: usize = 9;
fn read_command_line() -> [i32; SIZE] {
    [10, 32, 12, 43, 52, 53, 83, 2, 9]
}
fn check_size(size: usize) {
    if size == 0 {
        panic!("Size is of tab = 0.");
    }
}
fn min_i32(lhs: i32, rhs: i32) -> i32 {
    if lhs < rhs {
        lhs
    } else {
        rhs
    }
}
fn find_min(tab: [i32; SIZE]) -> i32 {
    check_size(SIZE);
    let mut min = i32::MAX;
    for i in 0..SIZE {
        min = min_i32(min, tab[i]);
    }
    min
}
fn main() {
let min = find_min(read_command_line());
println!("The minimal value is: {min}");
}

Le code

const SIZE: usize = 9;
fn read_command_line() -> [i32; SIZE] {
    [10, 32, 12, 43, 52, 53, 83, 2, 9]
}
fn check_size(size: usize) {
    if size == 0 {
        panic!("Size is of tab = 0.");
    }
}
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]) -> i32 {
    check_size(SIZE);
    let mut min = i32::MAX;
    for t in tab {
        min = min_i32(min, t);
    }
    min
}
fn main() {
    let tab = read_command_line();
    println!("Among the numbers in the list:");
    print_tab(tab);
    let min = find_min(tab);
    println!("The minimal value is: {min}");
}

Rustlings

Les rustlings à faire dans ce chapitre sont les suivants:

Les ifs

$ rustlings run if1
$ rustlings run if2
$ rustlings run if3

Les fonctions

$ rustlings run functions1
$ rustlings run functions2
$ rustlings run functions3
$ rustlings run functions4
$ rustlings run functions5

Quiz

$ rustlings run quiz1