Introduction

Ce livre est une introduction au langage de programmation Rust, destiné aux débutants en programmation. Il est divisé en 3 partie:

La première partie vous expliquera ce qu'est Rust, ses différences avec d'autres langages ainsi qu'un aperçu général autour de ses fonctionnalités. La deuxième partie vous donnera un avant-goût de son utilisation à travers différents domaines de la programmation ainsi que des exemples de cette utilisation. Le dernier chapitre vous montrera comment construire une "Todo" app en ligne de commande en vous expliquant chaque étape

Vous pourrez aussi trouver à la fin de ce livre quelques ressources additionnelles vous permettant d'approfondir le sujet et découvrir Rust.

J'ai écris ce livre pour m'essayer à la crate mdBook (Une crate est un projet Rust publié sur crates.io afin d'être partagé à la communauté, vous aurez plus d'informations dans la section "Écosystème de Rust") et d'approfondir mes connaissances concernant Rust ainsi que des concepts de programmation plus globaux. Il s'agit d'un projet personnel qui, je l'espère, vous permettra de mieux comprendre Rust.

Mais je reste un débutant dans le domaine, je n'ai actuellement que peu de connaissances au niveau de Rust et de la programmation en générale, je me suis lancé dans une reconversion professionnelle et je voulais partager un peu de ce que j'ai pu apprendre depuis. Dans ce contexte, je suis ouvert aux propositions de correction et de modifications, n'hésitez pas à ouvrir une issue sur le repository GitHub

Logo du Rust Programming Language

Pourquoi Rust ?

Rust est un langage de programmation bas-niveau, orienté performances et sécurité mais aussi un langage multi-paradigme que l'on trouve généralement plus souvent dans les langages de haut-niveau tel que C#, Java, Go, Python ou encore JavaScript. La popularité croissante de Rust crée une opportunité pour les programmeurs de découvrir un mélange de certains des plus grands paradigmes de langage de programmation des langages passés. Par multi-paradigme on entends:

Il a été créé par Graydon Hoare en 2006 en tant que projet personnel pendant qu'il travaillait à Mozilla Research. Mozilla à commencé à sponsoriser le projet en 2009 et l'a annoncé en 2010. C'est donc un langage plutôt jeune, La release pre-alpha est arrivé en Janvier 2012 et la première version stable, Rust 1.0, est sortie le 15 mai 2015.

En août 2020, Mozilla se voit obligé de se diriger vers une restructuration suite à l'impact de la pandémie de COVID-19. Cette restructuration à visé une grande partie de l'équipe Rust ainsi que l'équipe de Servo qui a été complètement dissoute (le projet est désormais affilié à la Linux Foundation). Cette situation a généré beaucoup d'incertitudes et de confusion sur le projet Rust en lui-même. D'un coté, des sociétés comme AWS (Amazon Web Services) ou Microsoft ont montré un grand soutien à Rust en investissant et en récupérant des membres de l'équipe Rust ayant quitté Mozilla afin de travailler sur leurs infrastructures.

La [Rust Core Team] à alors annoncé la création d'un Fondation Rust indépendante pour s'occuper de l'avenir de Rust et son écosystème. Cette fondation a fait sa première annonce en ligne le 8 Février 2021. En plus de plusieurs acteurs de l'écosystème Rust, on peut voir qu'AWS, Google, Huawei, Microsoft et Mozilla font partie des membres de cette foundation.

Les trois plus gros avantages qu'on connaît à ce langage sont les suivants:

  • Sécurité et gestion de la mémoire,
  • Accès concurrentiel plus facile grâce au modèle d'accès aux données,
  • Abstractions à coût nulle.

le langage de programmation Rust a gagné beaucoup d'attention de la communauté. Il a également été désigné "langage de programmation le plus aimé" par le sondage développeur de StackOverflow chaque année depuis 2016, ce qui indique que beaucoup de ceux qui ont l'occasion d'utiliser Rust en sont tombés amoureux. Il est très apprécié car il semblerait qu'il puisse résoudre les problèmes présents dans de nombreux autres langages, offrant un pas en avant solide avec un nombre limité d'inconvénients.

La communauté de développeurs Rust à tendance à s'agrandir depuis ces dernières années, même si il revient assez souvent que ce n'est pas un langage très facile à apprendre. Lors du dernier sondage Rust, beaucoup de développeurs ont montré qu'ils aimeraient plus de documentation et d'entraînement à l'avenir. Très peu d'utilisateurs hebdomadaire se disent experts en Rust, c'est clairement un langage qui demande du temps pour être maîtrisé. Rust est utilisé par des compagnies, grandes ou petites en production à travers le monde, comme Mozilla, Microsoft et AWS (évidemment), mais aussi Dropbox, npm, Figma ou encore Yelp.

Bien que Rust soit fermement attaché à la stabilité et à la rétrocompatibilité, cela n'implique pas que le langage soit finalisé. Un problème spécifique peut ne pas avoir accès aux fonctionnalités du langage qui le rendrait plus simple à exprimer. Aussi, le compilateur Rust est construit sur LLVM, ce qui signifie que le nombre de plate-formes cibles sera inférieur à C ou C++. Mais il semblerait que les équipes de Rust s'attellent à faire en sorte que la stabilisation des nouvelles fonctionnalités s'améliorent toujours plus, et il semblerait que les utilisateurs du compilateur nightly soit en train de se diriger de plus en plus vers la version stable, ce qui montre que les nouvelles fonctionnalités sont prises en charge rapidement.

Ce starter project sur GitHub vous permet de commencer avec le compilateur de Rust et le système de Cargo sans avoir à installer de toolchain logiciels. Vous pouvez utiliser l'IDE VSCode en ligne directement avec ce projet.

Un langage fortement typé

Rust est un langage typé statiquement, cela veut dire que le type des variables doit être connu au moment de la compilation.

Pour certains langages, cela veut dire que le programmeur doit spécifier de quel type est chaque variable (comme Java, C ou C++). Cela impose une lourde charge au programmeur, l'obligeant à répéter le type d'une variable plusieurs fois, ce qui peut potentiellement entraver la lisibilité et la refactorisation dans des projets plus conséquents.

Tandis que d'autres langages utilisent une forme d'inférence de type, ils sont capable de déduire le type d'une variable et le programmeur ne devra la spécifier que si le compilateur n'est pas capable de l'inférer pour nous (comme Haskell, Scala ou Kotlin). Certains langages à typage statique permettent l'inférence de type de programme entier. Bien que pratique lors du développement initial, cela réduit la capacité du compilateur à fournir des informations d'erreur utiles lorsque les types ne correspondent plus.

Rust apprends de ces deux styles et exige que les éléments de premier niveau tels que les arguments de fonction et les constantes aient des types explicites, tout en permettant l'inférence de type à l'intérieur du corps de la fonction. Il fait de son mieux penser plus loin programmeur tout en encourageant la maintenabilité à long terme

Dans cet exemple, le compilateur de Rust infère le type de twice, 2, et 1 car le paramètre value et le type de retour sont déclaré comme entiers signé sur 32 bits

fn simple_math(value: i32) -> i32 {
    let twice = value * 2;
    twice - 1
}

Un langage typé dynamiquement, par contre, doit connaître le type des variables au moment de l'exécution. Cela veut dire que le programmeur peut écrire un peu plus vite car il n'a pas à spécifier le type à chaque fois. Python, JavaScript ou Ruby sont des langages typés dynamiquement.

Le principal avantage de Rust ici est que toute vérification de type peut être faite par le compilateur, ce qui implique que beaucoup de bugs peuvent être détectés durant le développement. Cela ne veut pas dire que tous les systèmes de type statique sont équivalent. De nombreux langages à typage statique ont une grande astérisque à côté d'eux: ils permettent le concept de NULL.

Le concept de NULL permet à toute valeur d'être ce qu'elle dit ou rien, créant effectivement un deuxième type possible pour chaque type. Comme Haskell et quelques autre langages de programmation moderne, Rust encode cette possibilité en utilisant un type optionnel, et le compilateur vous demande de gérer le cas None. Cela empêche les occurrences de la redoutée erreur d'exécution TypeError: Cannot read property 'foo' of null (ou l'équivalent du langage), au lieu de la promouvoir comme une erreur de compilation que vous pouvez résoudre avant qu'un utilisateur ne le voie.

Voici en exemple une fonction pour saluer quelqu'un en fonction de si l'on connaît son nom ou non.

fn greet_user(name: Option<String>) {
	match name {
		Some(name) => println!("Bonjour, {}", name),
		None => println!("Bienvenue à vous, étranger!"),
	}
}

Si l'on oublie le cas None dans le match ou aurait essayé d'utiliser name comme si il s'agissait d'une valeur String toujours présente, le compilateur se plaindra.


#![allow(unused_variables)]
fn main() {
fn greet_user(name: Option<String>) {
    match name {
        Some(name) => println!("Bonjour, {}", name),
    }
}
}

Note: n'hésitez pas à tester le code lorsque vous voyez un bouton play dans le bloc de code. Dans ce cas, cela vous permettra de voir l'erreur renvoyé par le compilateur.

Sans garbage collector

Tous les programmes doivent gérer la façon dont ils utilisent la mémoire d'un ordinateur pendant l'exécution. Certains langages ont un "garbage collector" ("ramasse-miette" en Français) permet de supprimer les objets non utilisés en mémoire. C'est une sorte de gestionnaire de mémoire automatique, il va vérifier la mémoire pour libérer l'espace occupé par des données qui ne sont plus utilisées par le programme. Le garbage collector est implémenté différemment dans chaque langage, les langages de programmation haut-niveau ont d'office cet outil tandis que les langages bas-niveau le font grâce à des bibliothèque de code. Dans d'autres langages, le programmeur doit explicitement allouer et libérer la mémoire.

Rust utilise une troisième approche: la mémoire est gérée via un système d'ownership (propriété en français) avec un ensemble de règles que le compilateur vérifie au moment de la compilation. Il n'utilise pas de garbage-collector car il vérifie dès la compilation si une erreur est susceptible de survenir du a un mauvais usage de la mémoire par le développeur. L'"Ownership" permet un lien entre une variable et sa valeur. Une fois la valeur transmise a une autre variable, la première variable n'est plus accessible.

Rust vous donne le choix de stocker des données sur "the stack" ou "the heap" (la pile ou le tas en français), ce sont tous les deux des emplacements de la mémoire mais qui sont organisés différemment.

La pile enregistre les valeurs dans l'ordre de réception et enlève les valeurs dans l'autre sens selon le principe de dernier entré premier sorti, toutes les données doivent avoir une taille fixe et connue au moment de la compilation.

Lorsque vous ajoutez des données sur le tas, les données ont parfois une taille qui peut changer, vous demandez en fait une certaine quantité de mémoire, le gestionnaire de mémoire va trouver un emplacement sur le tas qui est suffisamment grand, va le marquer comme étant en cours d'utilisation et retournera un pointeur avec l'adresse de cet emplacement.

Le compilateur détermine au moment de la compilation quand la mémoire n'est plus nécessaire et peut être nettoyée. Cela permet une utilisation efficace de la mémoire ainsi qu'un accès à la mémoire plus performant en plus du fait de ne pas avoir un garbage collector qui fonctionne en permanence. Et grâce au concept d'ownership les programmeurs Rust peuvent écrire des programmes sans devoir allouer manuellement la mémoire (comme on doit le faire en C/C++). Vous pourrez en apprendre plus en pratique sur ce concept dans le projet à la fin de ce livre.

Tilde, un des premiers utilisateurs de Rust en production dans leur produit Skylight, a découvert qu'il étaient en mesure de réduire leur utilisation de la mémoire de 5GiB à 50Mib en réécrivant certains points de terminaison HTTP Java dans un Rust idiomatique. Des économies comme celle-ci s'additionnent rapidement lorsque les fournisseurs de cloud facturent des prix plus élevés pour une mémoire accrue ou des machines supplémentaires.

Les projets Rust sont bien adaptés, grâce à cette spécificité pour être utilisés comme bibliothèques par d'autres langages de programmation via des interfaces de fonctions étrangères. Cela permet aux projets existants de remplacer les éléments essentiels aux performances par du code Rust rapide sans les risques de sécurité de la mémoire inhérents aux autres langages de programmation de systèmes. Certains projets ont même été réécrits progressivement en Rust en utilisant ces techniques.

Gestion d'erreur

En informatique, lors de l’exécution d’un programme, il y a toujours un risque d’erreur. C’est la raison pour laquelle tout bon langage possède une gestion des erreurs qui correspond concrètement en l’affichage d’un message d’erreur nous informant sur le type d’erreur détectée et (souvent) en l’arrêt de l’exécution du programme après que l’erreur ait été détectée.

Vu que Rust se veut fiable, il se doit de pouvoir gérer les situations où quelque chose ne se passerait pas comme prévu. Rust place les erreurs en deux catégories majeures, récupérable ou non-récupérable. Dans le cas d'une erreur récupérable, il est raisonnable de signaler le problème et retenter l'opération par exemple. Dans le cas d'une erreur irrécupérable, l'exécution du programme se stoppera, votre programme panique dans les termes Rust.

$ Cargo run
	Compiling a-program v0.1.0 (file:///project/a-program)
	 Finished dev [unoptimized + debuginfo] targets(s) in 0.6s
	  Running `target/debug/a-program.rs`
thread 'main' panicked at 'Your software sucks if it cannot handle errors!', src/main.rs:7:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
error: process didn't exit successfully: `target/debug/a-program.rs` (exit code: 101)

Ces exigences rendent votre code plus robuste car vous pourrez gérer les erreurs avant que votre code ne soit déployé en production. L'une des fonctionnalités les plus appréciées de Rust est son compilateur agressif qui vous aide à garantir l'exactitude et la sécurité avant même que le programme ne s'exécute. En conséquence, les développeurs Rust peuvent écrire des programmes hautement performants mais sûrs.

Le système de type robuste de Rust et l'accent mis sur la sécurité de la mémoire, le tout appliqué au moment de la compilation, signifient qu'il est extrêmement courant d'obtenir des erreurs lors de la compilation de votre code. Lorsque vous rencontrez une erreur de compilation en Rust, le compilateur vous donne immédiatement une explication de l'erreur, et vous suggère des solutions pour réparer cette erreur basé sur le contexte de votre programme.

Les développeurs Rust ont passé beaucoup de temps à améliorer les messages d'erreur pour s'assurer qu'ils sont clairs et exploitables. Vous pouvez aussi trouver un index reprenant les différentes erreur de compilation, la qualité de conception des messages d'erreur constitue un véritable avantage. Alors que d'autres langages de programmation de revoient que des erreurs vagues, Rust fournis des informations utiles et pertinentes sur la façon de corriger l'erreur.

Il est particulièrement courant d'entendre quelqu'un se plaindre d'avoir "combattu le vérificateur d'emprunt". Bien que des erreurs puissent être décourageantes, il est important de reconnaître que chacun des emplacements identifiés étaient susceptible d'introduire bugs et vulnérabilités potentielles dans un langage qui n'effectue pas les mêmes vérifications.

Voyons un peu comment se gère une erreur de manière pratique, dans cet exemple, on crée une chaîne mutable contenant un nom, qui prend ensuite en référence les trois premiers bytes du nom. Bien que cette référence soit suspendue, nous tentons de muter la chaîne en l'effaçant. Il n'y a désormais aucune garantie que la référence pointe vers des données valide et la déréférencer pourrait amener à un "Undefined behavior" (un comportement indéfini en français), donc le compilateur nous arrête:


#![allow(unused_variables)]
fn main() {
fn no_mutable_aliasing() {
	let mut name = String::from("Vivian");
	let nickname = &name[..3];
	name.clear();
	println!("Hello there, {}!", nickname);
}
}

Heureusement, le message d'erreur incorpore notre code et fais de son mieux pour expliquer le problème, pointant vers les emplacements exact.

L'écosystème de Rust

L'expérience Rust est plus vaste qu'une spécification de langage et un compilateur, de nombreux aspects de la création et le maintenance de logiciels sont dans le viseur des équipes Rust.

Plusieurs chaînes d'outils Rust simultanées peuvent être installés et gérées via rustup. L'installation de Rust est livrée avec Cargo, un outil en ligne de commande permettant de gérer les dépendances, exécuter des tests, générer de la documentation, etc. Étant donné que les dépendances, les tests et la documentation sont disponibles de base, leur utilisation est répandue.

De plus, Rust cherche à créer un certain standard pour permettre à tout le monde de travailler sur les mêmes bases. Vous pourrez voir que Rust propose plusieurs outils de développement, un gestionnaire de paquet et possède un système de documentation bien à lui.

Comme je le disais précédemment, plusieurs chaînes d'outils Rust simultanées peuvent être installés et gérées via rustup.

Cela veut dire qu'il installe les composants du langage Rust depuis les canaux officiels et vous permet de changer entre le compilateur rustc stable, beta ou nightly tout en les gardant à jour.

Avec le compilateur de Rust lui-même, Rust vient avec un outil nommé Cargo. Cargo est le gestionnaire de paquets de Rust. Il télécharge les dépendances de vos paquets, les compiles, les rends distribuables puis les charge sur crates.io.

Crates Rust

Un des aspects les plus important de l'écosystème d'un langage de programmation est la manière de partager du code avec d'autres développeurs. Pour Rust, on passe par des packages, plus souvent appelé "crates".

Crates.io est le site communautaire de partage et de découverte des bibliothèques de code Rust. Tout bibliothèque publiée sur crates.io verra sa documentation construite et publiée sur docs.rs. Et il existe une alternative non officielle, libs.rs

Rust est encore relativement nouveau, ce qui signifie que certaines bibliothèques souhaitées ne sont peut-être pas encore disponibles. L'avantage est qu'il y a beaucoup de terrain fertile pour développer ces bibliothèques nécessaires, peut-être même en profitant des développements récents dans des domaines de l'informatique pertinents. En raison de cela et des capacités de Rust, certaines des bibliothèques de code en Rust, comme la crate Regex, sont les meilleurs dans tout langages confondu.

Outils Rust

Les équipes de Rust mettent plusieurs outils différents permettant un développement rapide ainsi qu'une collaboration plus facile orientant votre travail sur ce qui vraiment.

Rust fmt est un outil vraiment pratique que vous pouvez utiliser pour formater votre code en suivant un modèle consistant. Plus de perte de temps en configuration. Il vous suffit d'utiliser cargo fmt et votre code aura le même format que n'importe quel autre code Rust.

cargo check va tenter de compiler votre code sans le lancer: c'est une commande très utile en développement, lorsque vous voulez juste vérifier que votre code est correct sans le lancer.

Rust est livré avec une suite de test intégrés: cargo test.

Des lints supplémentaires sont disponible depuis Clippy.

Le support IDE est de plus en plus performant chaque jour grâce notamment à des projets comme rust-analyzer ou intellij Rust plugin.

En plus des outils built-in, La communauté Rust a créé un grand nombre d'outils de développement. Benchmarking (criterion), fuzzing (cargo-fuzz) et property-based testing sont tous facilement accessibles et bien utilisés dans les projets

Documentation Rust

La documentation traditionnelle consiste typiquement en livres entiers et site webs. La nouvelle génération de développeurs veut de la meilleure documentation en plus grande quantité. En tant que "nouveau" langage, Rust est déjà à la pointe de l'innovation en matière de documentation de langage de programmation.

Rust fournis un outil nommé rustdoc, son boulot est de générer de la documentation pour les projets Rust.

Les sites de documentation Rust comme docs.rs et Rust by Example (et sa version étendue) utilisent le Rust Playground pour faire tourner des exemples de code Rust directement depuis le navigateur. Ces livres intéractifs sont bien mieux que du simple texte.

Communauté Rust

Au delà des points techniques, Rust possède une communauté dynamique et accueillante. Il existe plusieurs moyens officiels et non officiels permettant aux gens d'obtenir de l'aide, tels que:

Rust a un code de conduite appliqué par une équipe de modération impressionnante pour s'assurer que les espaces officiels soient accueillants, et la plupart des espaces non officiels observent également quelque chose de similaire.

Hors ligne, (même si la période Covid à chamboulé pas mal de chose) Rust a plusieurs conférences autour du globe comme RustConf, Rust Belt Rust, RustFest, Rust Latam, RustCon Asia, Rusty Days, RustLab, etc...

Programmation système

La programmation système est un type de programmation qui vise au développement de programmes qui font partie du système d'exploitation d'un ordinateur ou qui en réalisent les fonctions. On peut trouver des langages comme le langage assembleur, C ou C++.

Un des plus grand bénéfice de l'utilisation d'un langage de programmation système est son habilité à contrôler des détails bas-niveau. Pour beaucoup de gens, Rust est largement considéré comme une alternative aux autres langages de programmation de systèmes. En tant que langage destiné à remplacer C et C++, la plupart des gens supposent que Rust serait utilisé dans la programmation d'infrastructure, comme les systèmes d'exploitation, les bibliothèques natives et les plate-formes d'exécution.

Rust a comme objectif de sécuriser la programmation système, rendre la programmation système plus sûre permet à davantage de développeurs de se sentir habilités à écrire des programmes systèmes. Impliquer davantage de personnes dans la programmation système signifie qu'un plus grand nombre de profils et d'expériences pourra comprendre et produire des programmes systèmes.

Les langages de programmation systèmes s'attendent implicitement à une très longue période d'activité. Bien que certains développements modernes ne nécessitent pas autant de longévité, de nombreuses entreprises veulent savoir que leur base de code fondamentale sera utilisable dans un avenir prévisible. Rust reconnaît cela et a pris des décisions de conception consciente concernant la compatibilité ascendante et la stabilité. C'est un langage conçu pour les 40 prochaines années.

Le plus grand avantage que Rust peut offrir par rapport à ces langages est le borrow checker (Vérificateur d'emprunt en français). C'est la partie du compilateur chargée de s'assurer que les références ne survivent pas aux données auxquelles elles se réfèrent, et cela aide à éliminer des classes entières de bugs causées par l'insécurité de la mémoire.

Contrairement à de nombreux langages de programmation de systèmes existants, Rust ne vous oblige pas à passer tout votre temps dans les moindres détails. Rust s'efforce d'avoir autant d'abstractions à coût nul que possible, des abstractions aussi performantes que le code manuscrit équivalent. Dans cet exemple, nous montrons comment les itérateurs, une abstraction principale de Rust, peuvent être utilisés pour créer succinctement un vecteur contenant les dix premiers nombres carrés.


#![allow(unused_variables)]
fn main() {
let squares: Vec<_> = (0..10).map(|i| i * i).collect();
println!("{:?}", squares);
}

Je vous laisse un thread reddit qui vous orientera vers des ressources qui vous permettront de commencer en programmation système ainsi qu'un liste de différents outils orienté programmation système

Safe et Unsafe

Lorsque la partie "safe" de rust n'est pas capable d'exprimer certains concepts, vous pouvez utiliser la partie "unsafe" de Rust. Cela débloque quelques pouvoirs supplémentaires, mais en échange, le programmeur est responsable de s'assurer que le code est vraiment sûr. Ce code unsafe peut ensuite être développé dans des abstractions de niveau supérieur qui garantissent que tous les utilisations de l'abstraction sont sûres.

Utilisez du code unsafe doit être une décision calculée, car l'utiliser correctement nécessite autant de réflexion et d'attention que tout autre langage dans lequel vous êtes responsable d'éviter un "undefined behavior" (comportement indéfini en français). Réduire au minimum le code unsafe est le meilleur moyen de minimiser les possibilités d'erreur de segmentations et de vulnérabilités dues à l'insécurité de la mémoire.

Programmation orientée objet

La programmation orientée objet consiste à créer et faire interagir des "briques" logicielles qu'on appelle Objets. Et un objet représente une idée, un concept ou toute entité du monde physique. La programmation orientée objet est aussi divisée en deux catégories, les langages à classes (fonctionnelle ou impérative ou encore les deux) ou les langages à prototype. On peut retrouver ce paradigme avec des langages comme Python ou Ruby.

l'utilisation de la programmation orienté objet en Rust peut différer de ce que l'on connaît dans d'autres langages de programmation. Prenons les quatre pilier de l'OOP (Object Oriented Programming) qui sont l'encapsulation, l'héritage, l'abstraction et le polymorphisme et voyons comment Rust fais pour s'adapter.

  • Encapsulation
    • L'encapsulation consiste à regrouper les données et les informations dans une seule unité appellée Classe. Malheureusement, il n'y a pas d'implémentation de classe en Rust mais on peut réaliser une encapsulation en utilisant des struct.
  • Héritage
    • L'héritage est un moyen de réutilisé du code et de sécurité de type. Dans la réutilisabilité du code, nous pouvons implémenter le comportement d'un type et réutiliser l'implémentation dans différentes sous-classes. Dans Rust, il n'y a pas de concepts d'héritage des propriétés d'une structure. Au lieu de cela, lorsque vous concevez la relation entre les objets, ses fonctionnalités seront définies par une interface (un trait en Rust). Cela favorise la composition plutôt que l'héritage, ce qui est considéré comme plus utile et plus facile à étendre à des projets plus important.
  • Abstraction
    • L'abstraction vous permet d'afficher uniquement les informations nécessaires et de masquer tous les autres détails. Avec Rust, on y arrive grâce au mot-clé pub pour décider quel module, type, fonctions et méthode du code doivent être publics, et par défaut tout le reste est privé.
  • Polymorphisme
    • Le polymorphisme est un concept consistant à fournir une interface unique à des entités pouvant avoir différents types. Rust y arrive, encore une fois, un peu différemment grâce à l'utilisation de generics.

Le fait que Rust permette une implémentation différente de l'OOP pourrait rendre l'apprentissage de Rust plus compliqué pour des développeurs venant d'autres langages orienté objet, dans le sens où ils doivent modifier leurs habitudes et la façon de penser leur code. Aussi, certaines définitions classifierait Rust comme étant un langage orienté objet, d'autres non. Cela vient du fait qu'une structure ne peut pas hérité des champs et fonctions d'une structure parente.

Programmation asynchrone

L'synchronie, dans la programmation, fais référence à l'occurrence d'événements indépendants du flux principal du programme et des moyens de gérer ses événements. Cela permet à une unité de travail de s'exécuter séparément du thread d'application principal, puis de lui notifier quand le travail a été exécuté avec succès ou non. Cela permet de meilleures performances et une réactivité accrue.

Certains parties du "Rust asynchrone" sont prise en charge avec les mêmes garanties de stabilité que le "Rust synchrone", d'autres parties sont encore en cours de maturation et changeront avec le temps. Le "Rust asynchrone" est plus difficile à utiliser et peut entraîner une charge de maintenance plus élevée, mais vous offre en retour les meilleurs performances de sa catégorie. Tous cela s'améliore constamment, de sorte que l'impact de ces problèmes s'estompera avec le temps. A titre d'exemple, Rust a des futures asynchrones depuis plus de trois ans, mais le support stable async/await dans le langage lui-même n'a que quelques mois.

Lorsque l'on parle de programmation asynchrone, on entends souvent parler de concurrence. C'est la capacité de différentes parties ou unité d'un programme à être exécutée dans le désordre ou dans un ordre partiel, sans affecter le résultat final et permettre une exécution parallèle des unités.

Ces dernières années, deux nouveaux langages de programmation ont gagné en popularité auprès des développeurs. L'un est Rust et l'autre est Go. Une grande partie de leur Succès est leur prise en charge supérieure des modèles de programmation d'accès concurrentiel.

En fait, l'un des premiers slogans de Rust est "fearless concurrency" (concurrence sans peur en français). Il promet la productivité des développeurs dans l'écriture de programmes multi-threads asynchrones optimisés pour les architectures de processeurs multicœurs d'aujourd'hui. Comme l'a démontre Node.js, une programmation asynchrone simple est cruciale pour le succès d'un langage ou d'un framework côté serveur.

Si la programmation asynchrone vous intéresse, sachez que 4 des 10 plus importantes crates Rust, tokio, async, futures et hyper sont des framework pour des applications multi-thread asynchrone et Rust fournis un book officiel concernant la programmation asynchrone.

Interopérabilité entre langages

l'interopérabilité est une caractéristique d'un produit ou d'un système lui permettant de fonctionner avec d'autres produits ou système existant et ainsi permettre à des outils différents de communiquer et travailler ensemble.

A mesure que l'adoption de Rust se développe, les développeurs doivent de plus en plus intégrer les programmes Rust avec des programmes écrits dans d'autres langages. Dans le passé, C et C++ étaient les langages les plus courants pour "parler" à Rust car ils sont utilisés dans les projets de logiciels d'infrastructure.

Maintenant que Rust se développe dans les projets de logiciels d'applications, davantage d'interface et de ponts au niveau langage sont maintenant nécessaires. Un bon exemple est le Rust JavaScript bridge qui prend en charge les fonctions Rust dans les applications Node.js, l'approche d'applications hybride Rust + JavaScript prend de l'ampleur.

L'enquête a révélé que, outre C/C++ et JavaScript, les développeurs Rust sont intéressés par l'intégration avec R et Python. Cela indique l'intérêt des développeurs pour les applications d'apprentissage automatique, de big data et d'intelligence artificielle (IA).

En fait, de nombreux packages d'apprentissages automatique et statistiques Python et R sont implémentés dans des modules binaires natifs. Rust est l'un des meilleurs langages de programmation pour écrire des modules natifs. Cet exemple montre comment utiliser Rust pour exécuter des modèles Tensorflow dans une application Node.js.

A l'avenir, nous envisageons que ces modules Rust s'exécutent dans des conteneurs hautes performance gérés tels que WebAssembly.

Voici quelques crates qui pourraient vous aider à lier Rust avec un autre langage dans vos applications:

Blockchain

Le blockchain est un registre partagé et immuable qui facilite le processus d'enregistrement des transactions et de suivi des actifs dans un réseau d'entreprise. Un actif peut être tangible (maison, voiture, espèces,...) ou immatériel (propriété intellectuelle, droit d'auteur,...). Les entreprises fonctionnent grâce à l'information, et plus elle est reçu rapidement et précisément, et mieux c'est.

Le blockchain est une solution idéale pour cela, car les informations sont fournies immédiatement, partagées et totalement transparente qui sont stocké dans un registre immuable auquel seuls les membres autorisés du réseau peuvent accéder. En ce qui concerne les logiciels d'infrastructure, Rust brille vraiment en tant que langage de programmation pour les systèmes blockchain.

Grâce à l'adoption agressive de Rust par de grands projets de blockchain tels que Polkadot/Substrate, Oasis, Solana, ou encore Second State, l'intérêt pour le blockchain au niveau de Rust est relativement bien présent. A bien des égards, le blockchain convient parfaitement à Rust.

Il représente l'effort de la communauté pour reconstruire l'infrastructure d'Internet de manière décentralisée. Ils nécessitent un logiciel performant et très sûr. Si vous êtes intéressé par une carrière d'ingénieur blockchain, Rust est une compétence incontournable aujourd'hui.

On peut aussi le voir avec plusieurs guides ou projets pour orienter les ingénieurs blockchain dans leur passage à Rust comme Ethereum, Awesome Blockchain Rust ou encore rib.rs.

WebAssembly

WebAssembly (ou WASM) est un standard ouvert qui définit un format de code binaire portable pour les programmes exécutables, et un langage d'assemblage textuel correspondant, ainsi que des interfaces pour faciliter les intéractions entre ces programmes et leur environnement hôte. Le but principal de WebAssembly est d'activer des applications hautes performances sur des pages Web, mais aussi le format est conçu pour être exécuté et intégré dans d'autres environnements également, y compris autonomes.

Techniquement parlant, il s'agit d'un nouveau langage d'assemblage de bas-niveau qui fonctionne efficacement sur la plate-forme web existante qui permet de traduire des applications en modules prêts pour le web et qui peuvent s'exécuter dans les navigateurs modernes à des vitesses presque native.

WebAssembly est un environnement d'exécution populaire pour les programmes Rust et ils ont tout les deux été inventé par Mozilla.

Rust se concentre sur les performances et la sécurité de la mémoire, tandis que WebAssembly se concentre sur les performances et la sécurité d'exécution. En tant que conteneur d'exécution, WebAssembly rend également les programmes Rust multi-plateformes et plus facile à gérer. Il y a en effet beaucoup de synergie entre les deux technologies.

Il a été inventé au départ en tant que machine virtuelle côté client pour exécuter des applications dans le navigateur. Mais comme Java et JavaScript avant lui, WebAssembly effectue maintenant la migration du côté client vers le côté serveur.

La plupart des développeurs Rust travaillent aujourd'hui sur des backend d'applications Web. Pas étonnant que les crates comme hyper, actix-web et Rocket soient parmi les plus populaires auprès des développeurs Rust.

Vous pouvez démarrer le développement d'applications Rust et WebAssembly à partir de "starter project" dans ce repository GitHub. Et il existe un book pour ceux qui préfèrent la théorie.

programmation embarquée

Un système embarqué est un système informatique (processeur, mémoire, périphérique I/O) qui a une fonction dédiée dans un système mécanique ou électrique plus grand. Les systèmes embarqués modernes sont souvent basés sur des microcontrôleurs, mais les microprocesseurs ordinaires sont également courant, en particulier dans les systèmes plus complexes.

Les systèmes embarqués vont des appareils portables tels que les montres numériques et les lecteurs MP3, aux grandes installations fixes telles que les contrôleurs de feux de signalisation, les contrôleurs logiques programmables et les grands systèmes complexes tels que les véhicules, les systèmes d'imagerie médicale et l'avionique. La complexité varie de faible, avec un seul microcontrôleur, à très élevé avec plusieurs unités périphériques et réseaux montés à l'intérieur d'un grand rack d'équipement

Avec un accès direct au matériel et à la mémoire, Rust est un langage idéal pour le développement embarqué et le bare-metal programming. Vous pouvez écrire du code extrêmement bas-niveau, Comme le kernel d'un operating system ou une application pour microcontrôleur.

Les principaux types et fonctions de Rust ainsi que les bibliothèques de code réutilisable brillent dans ces environnements particulièrement difficiles.

A nouveau, Rust fournis un book permettant une approche plus théorique de la programmation embarqué en Rust, et j'ajoute une liste de ressource concernant la programmation embarqué et bas niveau en Rust

Il existe aussi plusieurs projets qui pourront vous permettre de commencer rapidement comme un tutorial pour créer un kernel monolithique pour Operating System embarqué ou ce blog post destiné aux développeurs expérimenté mais avec peu de connaissance en embarqué

Interface en ligne de commande

Une interface en ligne de commande (CLI pour Command Line Interface) traite les commandes d'un programme sous la forme de lignes de texte. Les systèmes d'exploitation implémentent une interface en ligne de commande dans un shell interactif aux fonctions ou services du système.

Cet accès a été principalement fourni aux utilisateurs par des terminaux à partir du milieu des années 60. Par rapport à une interface graphique, une interface en ligne de commande nécessite moins de ressources à implémenter.

Les applications peuvent également avoir des interfaces en ligne de commande. Certaines applications prennent en charge uniquement un CLI, présente une invite de commande à l'utilisateur et agissant sur les lignes de commande au fur et à mesure de leur saisie. D'autres programmes prennent en charge à la fois une interface en ligne de commande et une interface graphique.

Dans certains cas, une interface graphique est simplement un wrapper autour d'un fichier exécutable en ligne de commande distinct et dans d'autres cas, un programme peut fournir un CLI comme alternative à son interface graphique.

Rust est un langage rapide, compilé statiquement, avec de bons outils et un écosystem grandissant rapidement. Ce qui en fait un bon choix pour écrire des Command Line Interface. Ils peuvent être rapide, portable, et rapide à exécuter.

Les CLI sont aussi un bon moyen pour commencer à apprendre Rust, comme vous pourrez le faire avec le projet du chapitre 16 et Rust vous propose un book pour découvrir les applications en ligne de commande de manière plus théorique.

Je vous ajoute aussi une liste de ressources qui pourra soit vous aider dans la construction de votre propre application en ligne de commande, soit vous donner un aperçu du potentiel des application CLI en Rust.

Développement de jeu vidéo

Nous allons parler ici du processus de développement d'un jeu vidéo. Le développement de jeu commerciaux traditionnels est normalement financé par un éditeur et peut prendre plusieurs années pour être achevé. Les jeux indépendants prennent généralement moins de temps et d'argent et peuvent être produits par des particuliers ou de petites société.

L'industrie du jeu indépendant est en plein essor, facilitée par la croissance des logiciels de développement de jeux accessibles tel que Unreal Engine et de nouveaux systèmes de distribution en ligne tels que Steam ainsi que par le marché des jeux mobiles pour Android ou Appareils IOS.

Rust est apprécié dans le domaine du développement de jeu vidéo grâce à ses performances et sa sécurité par rapport aux langages qui ont pris le devant de la scène comme C++, car c'est un domaine où les performances et le maintien du code est très important.

Au niveau du développement de jeu vidéo, Rust propose quelques projets comme Bevy ou Amethystqui sont deux data driven game engine, macroquad un cross-platform game engine, il y a aussi Veloren un jeu multijoueur style RPG écrit en Rust et complètement Open Source ou encore Embark studio, un studio de jeu open source utilisant Rust en tant que langage primaire.

Je peux vous proposer le site Are we game yet ?, comme ressources additionnels, pour ceux qui voudraient s'orienter vers le développement de jeu vidéo. Il propose plusieurs jeux déjà écris en Rust ainsi qu'une liste plutôt conséquente d'outils qui vous permettront de creuser le sujet.

Créer un programme en ligne de commande

Ce tutoriel est ma version de celui fait par Claudio Restifo sur FreeCodeCamp et traduit en Français. Son but est de vous donner un aperçu pratique de certains concepts qui vous permettront de découvrir Rust.

Si vous préférez voir le code directement, vous pouvez accéder à ce repository GitHub.

Le créateur de ce tutoriel vient du monde de JavaScript, où il est traditionnel de faire une To-Do App comme premier projet. Être familier avec la ligne de commande est nécessaire car l'application va tourner dans le terminal et vous aurez aussi besoin de certaines connaissances des concepts de programmation généraux.

Nous allons stocker des valeurs en tant que collection d'item et une valeur booléenne représentera son état actif.

learn rust          false
write some code     true
play video games    false

Qu'est-ce que nous allons couvrir ?

Prêt à vous salir un peu les mains ? Let's go!

Commencer avec Rust

D'abord, téléchargez Rust sur votre ordinateur.

Pour les systèmes Unix comme Linux et MacOS, vous devez simplement ouvrir un terminal et écrire ceci:

$ curl https://sh.rustup.rs -sSf | sh

Si tout va bien, vous devriez voir ceci apparaître dans votre terminal.

Rust is installed now. Great!

Du coté de Windows, téléchargez et lancez rustup-init.exe. Cela va démarrer l'installation dans une console et vous devriez voir le message précédent si l'opération est un succès.

Pour d'autres options d'installation et informations, vous pouvez vous rendre sur la page getting started du site officiel de Rust. Là, vous trouverez également les instructions pour intégrer le langage à votre éditeur favori pour une meilleure expérience.

Débuter un projet

Pour commencer un nouveau projet, allez à l'endroit où vous voulez créer votre projet (comme un dossier /repos par exemple) et lancez simplement cargo new <project-name>. Dans mon cas, j'ai gardé le nom du tutoriel de base et utilisé "todo-cli". J'ai donc lancé:

$ cargo new todo-cli

Maintenant, naviguez vers le dossier fraîchement créé et listez son contenu. Vous devriez voir deux fichier à l'intérieur du dossier

Schema du contenu du dossier todo-cli

Nous allons travailler sur le fichier src/main.rs pour le reste de ce livre, donc allons-y et ouvrons-le:

fn main(){
    println!("Hello world!");
}

Comme beaucoup d'autres langages, Rust a une fonction main qui sera lancée en premier. fn est la mot-clé permettant de déclarer une fonction, tandis que le ! dans println! représente une macro. Comme vous l'avez peut être deviné, ce programme est la version Rust de "hello world!".

Pour le builder et le lancer, exécutez simplement cargo run.

$ cargo run
Hello world!

Note: Vous pouvez voir un bouton "play" en haut à droite de l'encart contenant le code Rust Hello world!, il vous permet de tester le code directement sur cette page. C'est comme si vous lanciez cargo run depuis votre terminal.

Comment lire les arguments

Notre but est d'avoir un CLI qui accepte deux arguments: le premier sera l'action, et le second sera l'item.

Nous allons commencer par lire les arguments saisis par l'utilisateur et les afficher. Remplacez le contenu de main avec ce qui suit:

fn main() {
    let action = std::env::args().nth(1).expect("Please specify an action");
    let item = std::env::args().nth(2).expect("Please specify an item");

    println!("{:?}, {:?})", action, item);
}

Commençons par digérer toutes ses informations, prenons cette ligne:

let action = std::env::args().nth(1).expect("Please specify an action")

Nous allons la décomposer pour voir ce qu'il se passe

let action =

le mot clé let lie une valeur à une variable (qui sera ici action).

std::env::args()

C'est une fonction du module env de la bibliothèque standard de Rust qui retourne l'argument avec lequel le programme a commencé.

.nth(1)

Comme l'argument retourné par la fonction précédente est un itérateur, nous pouvons accéder à la valeur stockée à chaque position avec la méthode nth(). L'argument a la position 0 étant le programme lui-même, on commence à lire au premier élément.

.expect("Please specify an action")

C'est une méthode définie pour l'enum Option qui pourra être la valeur, mais si elle n'est pas présente, le programme se terminera directement et retournera le message fourni qui sera ici "Please specify an action".

Comme le programme peut être lancé sans argument, Rust nous demande de vérifier si une valeur est réellement fournie en nous donnant un type Option: soit la valeur est là, soit non.

En tant que programmeur, nous avons la responsabilité d'assurer que nous prenons l'action appropriée dans chaque cas. Pour le moment, si l'argument n'est pas fourni nous allons quitter le programme directement

Lançons le programme et passons lui deux arguments. Pour faire cela, ajoutez-lez après --. Par exemple:

$ cargo run -- hello world!
    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/todo_cli hello 'world'\!''``
"hello", "world!"

Insérer et sauvegarder des données

Réfléchissons un instant à notre objectif pour le programme. Nous voulons lire l'argument fourni par l'utilisateur, mettre à jour notre todo list, et la stocker quelque part pour l'utilisation.

Pour faire cela, nous devons implémenter notre propre type pour lequel nous pouvons définir une méthode pour répondre à nos besoins.

Nous allons utiliser les struct de Rust, ce qui nous permettra de faire les deux de manière clean. Cela évite d'avoir à écrire tout le code à l'intérieur de la fonction main.

Comment définir notre structure

Notre fichier devrait ressembler à ceci:

use std::collections::HashMap;

fn main() {
    let action = std::env::args().nth(1).expect("Please specify an action");
    let item = std::env::args().nth(2).expect("Please specify an item");

    println!("{:?}, {:?}", action, item);
}

struct Todo {
    // use rust built-in HashMap to store key - val pairs
    map: HashMap<String, bool>,
}

Nous ajoutons ici deux éléments:

use std::collections::HashMap;

Étant donné que nous taperons beaucoup HashMap dans les étapes suivantes, nous pouvons l'amener dans la portée et nous éviter de le retaper. Ceci va nous permettre d'utiliser HashMap directement sans devoir taper le chemin complet à chaque fois.

struct Todo {
    map: HashMap<String, bool>,
}

Cela va définir notre type personnalisé Todo: une structure avec un seul champ nommé map. Ce champ est un HashMap. Vous pouvez le considérer comme une sorte d'objet JavaScript, où Rust nous oblige à déclarer les types de clé et de valeur.

Ajouter des méthodes à notre structure

Les méthodes sont comme des fonctions régulières, elles sont déclarées avec le mot clé `fn, elles acceptent des paramètres et ont une valeur de retour.

Toutefois, elles différent des fonctions régulières dans le sens où elles sont définies dans le contexte d'une structure et leurs premiers paramètres sont toujours self.

Nous allons définir un bloc impl en dessous de notre structure.

impl Todo {
    fn insert(&mut self, key: String) {
        // insert a new item into our map
        // we passe true as value
        self.map.insert(key, true);
    }
}

Cette fonction est assez simple: elle prend seulement une référence à la structure et une clé et l'insère dans votre map en utilisant la fonction intégrée insert de HashMap.

Deux informations très importantes:

  • mut rend une variable mutable. En Rust chaque variable est immuable par défaut. Si vous voulez mettre à jour votre valeur, vous devez activer sa mutabilité en utilisant le mot-clé mut.
    • Étant donné qu'avec notre fonction, nous modifions effectivement notre map en ajoutant une nouvelle valeur, nous avons besoin qu'elle soit déclarée mutable.
  • & indique une référence. Vous pouvez imaginer que la variable a un pointeur vers un emplacement de la mémoire où la valeur est stockée, à la place d'être la valeur elle-même.

Dans les termes Rust, il s'agit d'un borrow (emprunt en français), signalant que la fonction ne possède actuellement pas la valeur, mais pointe simplement vers l'emplacement où elle est stockée.

Aperçu du concept d'Ownership

Avec les conseils précédents sur l'emprunt et la référence, c'est maintenant le bon moment pour parler brièvement de l'Ownership(https://doc.rust-lang.org/book/ch04-00-understanding-ownership.html) (possession en français?).

Le système d'ownership possède trois règles:

  • Chaque valeur en Rust à une variable: son "Owner".
  • Il peut uniquement y avoir un "Owner" à la fois pour chaque valeur.
  • Lorsque le "Owner" sort de la portée, la valeur sera relâchée.

Rust vérifie ces règles au moment de la compilation, ce qui signifie que nous devons être explicite pour dire si et quand nous voulons qu'une valeur soit libéré en mémoire. Voyons un exemple:

fn main() {
    // the owner of the String is x
    let x = String::from("Hello");

    // we move the value inside this function.
    // now doSomething is the owner of x.
    // Rust will free the memory associated with x
    // as soon as it goes out of "doSomething" scope.
    doSomething(x);

    // The compiler will throw an error since we tried to use
    // but since we moved it inside "doSomething"
    // we cannot use it as we don't have ownership
    // and the value may have been dropped.
    println!("{}", x);
}

Ce concept est largement considéré comme le plus difficile à comprendre lors de l'apprentissage de Rust, car c'est un concept qui peut être nouveau pour de nombreux programmeurs.

Nous n'allons pas creuser plus en profondeur les tenant et aboutissant du système d'ownership. Pour le moment, gardez en tête les règles mentionnées plus haut. Essayez de penser, à chaque étape, si vous avez besoin de "posséder" les valeurs et de les supprimer, ou si vous avez besoin d'une référence pour qu'elle puisse être conservée.

Par exemple, dans la méthode insert ci-dessus, nous ne voulons pas posséder map, car nous en avons encore besoin pour stocker ses données quelque part. Ce n'est qu'alors que nous pourrons enfin libérer la mémoire allouée.

Sauvegarder map sur le disque

Étant donné que cette une app démo, nous allons adopter la solution la plus simple pour un stockage de long terme: écrire map dans un fichier sur le disque.

Créons une nouvelle méthode pour notre bloc impl.

impl Todo {
    fn insert(&mut self, key: String) {
        // insert a new item into our map.
        // We pass true as a value
        self.map.insert(key, true);
    }
    fn save(self) -> Result<(), std::io::Error> {
        let mut content = String::new();
        for (k, v) in self.map {
            let record = format!("{}\t{}\n", k, v);
            content.push_str(&record)
        }
        std::fs::write("db.txt', content)
    }
}

Décomposons ce que nous venons d'ajouter pour y voir plus clair:

fn save(self) -> Result<(), std::io::Error> {

-> annote le type de retour de la fonction. Nous retournons un Result.

let mut content = String::new();

On lie la variable mutable content à une nouvelle String.

for (k, v) in self.map {

On itère sur map.

let record = format!("{}\t{}\n", k, v);

On formate chaque chaîne de caractère, séparant les clés et valeurs avec un caractère "tab" et chaque ligne avec un retour à la ligne.

content.push_str(&record)

On pousse la chaîne formaté dans la variable content.

std::fs::write("db.txt", content)

On écrit content à l'intérieur du fichier nommé db.txt.

Il est important de noter que save prend possession de self. C'est une décision arbitraire pour que le compilateur nous arrête si nous essayons accidentellement de mettre à jour la map après avoir appelé save (car la mémoire de self serait libérée).

C'est une décision personnelle de "forcer" save à être la dernière méthode utilisée. Et c'est un exemple parfait pour vous montrer comment vous pouvez utiliser la gestion de mémoire de Rust pour créer du code plus strict qui ne pas pas compiler (qui aide à prévenir les erreurs humaines lors du développement).

Comment utiliser la structure dans main

Maintenant que nous avons ces deux méthodes, nous pouvons les utiliser. Nous avons laissé main au point où nous lisons les arguments fournis. Maintenant si l'action fournie est "add" nous allons insérer cet item dans le fichier et le stocker pour une utilisation future.

Notre fonction main devrait ressembler à ceci:

fn main() {
    let action = std::env::args().nth(1).expect("Please specify an action");
    let item = std::env::args().nth(2).expect("Please specify an item");

    println!("{:?}, {:?}", action, item);

    let mut todo = Todo {
        map: HashMap::new(),
    };
    if action == "add" {
        todo.insert(item);
        match todo.save() {
            Ok(_) => println!("todo saved"),
            Err(why) => println!("An error occurred: {}", why),
        }
    }
}

Voyons ce que nous avons là:

let mut todo = Todo {

on instancie un struct, défini comme mutable.

if action == "add" {

Si l'argument d'action est add:

todo.insert(item)

On appelle la méthode TODO insert en utilisant la notation . et on lui donne item comme argument

match todo.save() {
    Ok(_) => println!("todo saved"),
    Err(why) => println!("An error occurred: {}", why),
}

On match le Result renvoyé par la fonction save et affiche un message sur l'écran dans chaque cas.

Testons ça, lancez votre terminal et tapez:

$ cargo run -- add "code rust"
todo saved

Inspectons l'item sauvegardé:

$ cat db.txt
code rust true

Lire depuis un fichier

Pour le moment, notre programme a un défaut fondamental: chaque fois que nous utilisons "add" on écrase map au lieu de le mettre à jour. C'est à cause du fait que nous créons un nouveau map vide chaque fois qu'on lance le programme. Réglons cela.

Ajouter une nouvelle fonction dans TODO

Nous allons implémenter une nouvelle fonction pour notre structure Todo. Une fois appelée, elle va lire le contenu de notre fichier et nous rendre notre Todo peuplé avec nos valeurs précédemment stockées. Notez que ce n'est pas une méthode parce qu'elle ne prend pas self comme premier argument.

Nous voulons l'appeler new, c'est une convention Rust (comme HashMap::new() utilisé auparavant).

Ajoutons le code suivant à l'intérieur de notre bloc impl:

use std::{collections::Hashmap, io::Read, str::FromStr};

//main function

impl Todo {
    fn new() -> Result<Todo, std::io::Error> {
        let mut f = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .read(true)
            .open("db.txt")?;
        let mut content = String::new();
        f.read_to_string(&mut content)?;
        let map: HashMap<String, bool> = content
            .lines()
            .map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
            .map(|v| (v[0], v[1]))
            .map(|(k, v| (String::from(k), bool::from_str(v).unwrap()))
            .collect();
        Ok(Todo { map })
    }
// ...rest of the methods
}

Pas de soucis si cela semble un peu accablant. Nous utilisons un style de programmation plus fonctionnel, principalement pour mettre en valeur et introduire le fait que Rust prend en charge plusieurs paradigmes trouvé dans d'autres langages comme les itérateurs, closures et fonctions lambda.

Voyons ce qu'il se passe ici:

use std::{collections::Hashmap, io::Read, str::FromStr};

On va ajouter use std::io::Read; et std::str::FromStr au début du fichier près de l'autre instruction use pour pouvoir utiliser les méthodes read_to_string et from_str (qui sont expliquées plus bas). Vu que tout les modules viennent de std, on peut les grouper dans un bloc avec {}.

fn_new() -> Result<Todo, std::io::Error> {

Nous avons défini une fonction new qui renvoie un Result qui sera soit un struct Todoou un io::Error.

let mut f = std::fs::OpenOptions::new()
    .write(true)
    .create(true)
    .read(true)
    .open("db.txt")?;

Nous avons configuré comment ouvrir le fichier db.txt en définissant plusieurs OpenOptions. Le plus notable est le flag create(true) qui va créer le fichier si il n'est pas déjà présent.

f.read_to_string(&mut content)?;

Cette méthode va lire tout les bytes dans le fichier et les ajoute dans la String content. Cette méthode à été importé avec sa déclaration use au début du fichier.

let map: HashMap<String, bool> = content

Nous devons convertir depuis le type String du fichier vers un HashMap. Nous le faisons en liant une variable map. C'est une des occasions où le compilateur a du mal à inférer le type pour nous, donc on le déclare par nous-même.

.lines()

lines crée un itérateur sur chaque ligne d'une String. Ce qui signifie que nous allons maintenant itérer sur chaque entrée de notre fichier, puisque nous l'avons formaté avec "/n" à la fin de chaque entrée.

.map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())

map prends une closure (|line|) et l'appelle sur chaque élément de l'itérateur, lines.splitn(2, '\t') divisera nos lignes sur le caractère tab (\t).

Et enfin collect::<Vec<&str>>() transforme un itérateur en une collection pertinente. Comme décrits dans la documentation, c'est l'une des méthodes les plus puissantes de la bibliothèque standard. Ici nous disons à la fonction map de transformer notre string divisé en un Vecteur (Vec<>)de string slices emprunté (<&str>) à la méthode. Ceci dit au compilateur quel collection nous voulons à la fin de l'opération.

.map(|v| (v[0], v[1]))

On transforme notre vecteur en un tuple par commodité.

.map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))

On va ensuite convertir les deux éléments du tuple en une String et un booléan. La méthode from_str() à été importé avec sa déclaration use au début du fichier.

.collect();

Nous les collectons enfin dans notre HashMap. Cette fois nous n'avons pas besoin de déclarer le type que Rust infère depuis la déclaration de liaison.

Ok(Todo { map })

Pour finir, si on ne rencontre pas d'erreur, on retourne notre structure au code appelant. Notez ici que, tout comme en JavaScript, nous pouvons utiliser une notation plus courte si la clé et la variable ont le même nom dans une structure.

Comment utiliser la fonction new

Dans main, mettez simplement à jour la liaison à notre variable todo avec:

let mut todo = Todo::new().expect("Initialisation of db failed);

Maintenant, si l'on retourne à notre terminal et lance un lot de commande "add", on devrait voir notre base de donnée se mettre à jour correctement:

$ cargo run -- add "make coffee"
todo saved
$ cargo run -- add "make pancakes"
todo saved
$ cat db.txt
make coffee     true
make pancakes   true

Une autre approche

Bien que map est généralement considéré plus idiomatique, ce qui précède aurait pu également être implémenté avec une boucle for à la place. Sentez-vous libre d'utiliser celle qui vous plait le plus.

impl Todo {
    fn new() -> Result<Todo, std::io::Error> {
        // open the db file
        let mut f = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .read(true)
            .open("db.txt")?;
        // read its content into a new string
        let mut content = String::new();
        f.read_to_string(&mut content)?;

        // allocate an empty HashMap
        let mut map = HashMap::new();

        // loop over each lines of the file
        for entires in content.lines() {
            // split and bind values
            let mut values = entries.split('\t');
            let key = values.next().expect("No Key");
            let val = values.next().expect("No Value");
            // insert them into HashMap
            map.insert(String::from(key), bool::from_str(val).unwrap());
        }
        // Return Ok
        Ok(Todo { map })
    }
}

Le code au dessus est fonctionnellement équivalent à l'approche plus "fonctionnelle" utilisé précédemment.

Mettre à jour une valeur

Comme dans toutes les applications TODO, nous voulons pouvoir non seulement ajouter des éléments, mais également les activer et les marquer comme terminés.

Ajouter la méthode complete

Pour faire cela, ajoutons une nouvelle méthode à notre struct nommé "complete". Dedans, on prend une référence à une clé, et on mets la valeur à jour, ou on retourne None si la clé n'est pas présente.

impl Todo {
    // rest of the TODO methods

    fn complete(&mut self, key: &String) -> Option<()> {
        match self.map.get_mut(key) {
            Some(v) => Some(*v = false),
            None => None,
        }
    }
}

Voyons ce qu'il se passe ici:

fn complete(&mut self, key: &String) -> Option<()> {

On déclare une nouvelle méthode complete qui prend une référence à une clé, on déclare aussi le type de retour: un Option vide.

match self.map.get_mut(key) {

Toute la méthode retourne le résultat de l'expression match avec self.map.get_mut qui peut être un Some() vide ou None.

Some(v) => Some(*v = false),

Si on récupère un Some, nous avons une donc une référence mutable à une valeur de clé. Nous utilisons l'opérateur * pour déréférencer la valeur et la définir sur false.

Comment utiliser la méthode complete

On peut utiliser la méthode complete de la même manière que nous avons utilisé la méthode insert auparavant.

Dans main, vérifiez que l'action passé en argument est "complete" en utilisant une instruction else if:

// in the main function

if action == "add" {
    // add action snippet
} else if action == "complete" {
    match todo.complete(&item) {
        None => println!("'{}' is not present in the list", item),
        Some(_) => match todo.save() {
            Ok(_) => println!("todo saved"),
            Err(why) => println!("An error occurred: {}", why),
        },
    }
}

Il est temps d'analyser ce que nous faisons ici:

} else if action == "complete" {

else if nous permet de d'avoir un argument add ou un argument complete.

match todo.complete(&item) {

On match l'Option retourné par la méthode todo.complete, on passe les items en tant que référence avec &item pour que la valeur soit toujours possédée par la fonction. Cela signifie que l'on peut l'utiliser dans notre macro println!, si nous n'avions pas fait cela, la valeur aurait été possédée par complete et abandonnée là.

None => println!("'{}' is not present in the list", item),

Si le cas est None, on affiche un avertissement à l'utilisateur pour une meilleure expérience.

Some(_) => match todo.save() {
    Ok(_) => println!("todo saved"),
    Err(why) => println!("An error occurred: {}", why),
}

Si l'on détecte qu'une valeur Some a été retournée, on appelle todo.save pour stocker les changements de manière permanente dans notre fichier. Err nous d'afficher un message à l'utilisateur si notre save rencontre une erreur.

Tentons de lancer le programme

Voilà, votre fichier src/main.rs devrait ressembler à ceci:

use std::{collections::HashMap, io::Read, str::FromStr};

fn main() {
    let action = std::env::args().nth(1).expect("Please specify an action");
    let item = std::env::args().nth(2).expect("Please specify an item");

    println!("{:?}, {:?}", action, item);

    let mut todo = Todo::new().expect("Initialisation of db failed");
    if action == "add" {
        todo.insert(item);
        match todo.save() {
            Ok(_) => println!("todo saved"),
            Err(why) => println!("An error occurred: {}", why),
        }
    } else if action == "complete" {
        match todo.complete(&item) {
            None => println!("'{}' is not present in the list", item),
            Some(_) => match todo.save() {
                Ok(_) => println!("todo saved"),
                Err(why) => println!("An error occurred: {}", why),
            },
        }
    }
}

struct Todo {
    // use rust built in HashMap to store key - val pairs
    map: HashMap<String, bool>,
}

impl Todo {
    fn new() -> Result<Todo, std::io::Error> {
        let mut f = std::fs::OpenOptions::new()
            .write(true)
            .create(true)
            .read(true)
            .open("db.txt")?;
        let mut content = String::new();
        f.read_to_string(&mut content)?;
        let map: HashMap<String, bool> = content
            .lines()
            .map(|line| line.splitn(2, '\t').collect::<Vec<&str>>())
            .map(|v| (v[0], v[1]))
            .map(|(k, v)| (String::from(k), bool::from_str(v).unwrap()))
            .collect();
        Ok(Todo { map })
    }
    fn insert(&mut self, key: String) {
        // insert a new item into our map.
        // We pass true as a value
        self.map.insert(key, true);
    }
    fn save(self) -> Result<(), std::io::Error> {
        let mut content = String::new();
        for (k, v) in self.map {
            let record = format!("{}\t{}\n", k, v);
            content.push_str(&record)
        }
        std::fs::write("db.txt", content)
    }
    fn complete(&mut self, key: &String) -> Option<()> {
        match self.map.get_mut(key) {
            Some(v) => Some(*v = false),
            None => None,
        }
    }
}

Il est temps d'essayer l'application que nous venons de développer avec notre terminal. Commençons par supprimer notre fichier de base de donnée pour un nouveau départ.

$ rm db.txt

Ensuite, ajoutons et modifions certains des todos:

$ cargo run -- add "make coffee"
$ cargo run -- add "code rust"
$ cargo run -- complete "make coffee"
$ cat db.txt
make coffee false
code rust true

Ce qui signifie qu'à la fin de ces commandes, nous avons une action faite ("make coffee") et une autre en attente "code rust".

Disons que nous devions faire du café à nouveau:

$ cargo run -- add "make coffee"
$ cat db.txt
make coffee true
code rust true

Utilisation d'une crate externe

Stocker les todos en JSON avec serde

Ce programme, même si minime, est lancé. Mais on peut toujours l'améliorer un peu. Venant du monde de JavaScript, l'auteur à décidé qu'à la place d'un fichier texte brut, il voulais stocker les valeurs dans un fichier JSON.

Nous allons prendre cette opportunité pour voir comment installer et utiliser un paquet depuis la communauté open source de Rust en passant par crates.io

Installer serde

Pour installer un nouveau paquet comme serde dans notre projet, ouvrons le fichier Cargo.toml. Dans le bas, vous devriez voir un champ [dependencies]: Ajoutez simplement la ligne suivante au fichier:

[dependencies]
serde_json = "1.0.60"

Et c'est tout. La prochaine fois que cargo va compiler notre programme, il va aussi télécharger et inclure le nouveau paquet avec notre code.

Mettre à jour Todo::new

Le premier endroit où nous voulons utiliser serde est lorsque l'on cherche à lire le fichier base de donnée. Maintenant, à la place de lire un fichier ".txt", on veut lire un fichier JSON.

Dans le bloc impl, mettons à jour la fonction new:

// inside Todo impl block

fn new() -> Result<Todo, std::io::Error> {
    // open db.json
    let f = std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .read(true)
        .open("db.json")?;
    // serialize json as HashMap
    match serde_json::from_reader(f) {
        Ok(map) => Ok(Todo { map }),
        Err(e) if e.is_eof() => Ok(Todo {
            map: HashMap::new(),
        }),
        Err(e) => panic!("An error occurred: {}", e),
    }
}

Les changements notables ici sont:

let f = std::fs::OpenOptions::new()

Plus de liaison mut f pour le fichier OpenOption, car nous n'avons plus besoin d'allouer manuellement le contenu dans une String comme auparavant. Serde s'en chargera pour nous.

open("db.json")?;

On mets à jour notre extension de fichier en tant que db.json.

match serde_json::from_reader(f) {

serde_json::from_reader désérialize le fichier pour nous.

Ok(map => Ok(Todo { map }))

Il intérfére avec le type de retour de map et va tenter de convertir notre JSON en un HashMap compatible. Si tout va bien on retourne notre struct Todo comme avant.

Err(e) if e.is_eof() => Ok(Todo {
    map: HashMap::new(),
}),

C'est un match guard qui nous permet d'affiner le comportement de l'expression match. Si serde retourne une erreur EOF (end of file) prématuré, cela signifie que le fichier est totalement vide (par exemple au premier lancement, ou si nous supprimons le fichier). Dans ce cas on récupère de cette erreur et on retourne un HashMap vide.

Err(e) => panic!("An error occurred: {}", e),

Pour toutes les autres erreurs, on quitte immédiatement le programme.

Mettre à jour Todo.save

L'autre emplacement où nous voulons utiliser serde est là où l'on sauvegarde notre map en tant que JSON. Pour faire cela, mettez à jour la méthode save dans le bloc impl pour devenir:

// inside Todo impl block
fn save(self) -> Result<(), Box<dyn std::error::Error>> {
    // open db.json
    let f = std::fs::OpenOptions::new()
        .write(true)
        .create(true)
        .open("db.json")?;
    // write to file with serde
    serde_json::to_writer_pretty(f, &self.map)?;
    Ok(())
}

Comme auparavant, voyons ce qui change ici:

fn save(self) -> Result<(), Box<dyn std::error::Error>> {

Cette fois on renvoie une Box contenant une implémentation d'erreur générique de Rust. Pour faire simple, une box est un pointeur vers une allocation en mémoire. Puisque nous pouvons retourner soit une erreur du système lors de l'ouverture du fichier, soit une erreur serde lors de sa conversion, nous ne savons pas vraiment laquelle des deux notre fonction peut renvoyer. Par conséquent, nous renvoyons un pointeur vers l'erreur possible, au lieu de l'erreur elle-même, afin que l'appelant puisse les gérer.

let f = std::fs::OpenOptions::new()

Comme pour new, plus de liaison mut f pour le fichier OpenOption, car nous n'avons plus besoin d'allouer manuellement le contenu dans une String.

open("db.json")?;

On mets évidemment à jour le nom du fichier en db.json pour rester cohérent.

serde_json::to_writer_pretty(f, &self.map)?;

Enfin, nous laissons serde faire le gros du travail et écrire notre HashMap sous forme de fichier JSON (pretty-printed).

Note: N'oubliez pas de retirer use std::io::Read; et use std::str::FromStr; au début du fichier, nous n'en avons plus besoin.

Et voilà.

Maintenant, vous pouvez lancer votre programme et vérifier la sortie enregistrée dans le fichier. Si tout s'est bien passé, vous devriez voir vos tâches enregistrées au format JSON.

Conclusion

J'espère que cette introduction vous aura appris quelque chose et éveillez votre curiosité. N'oubliez pas que nous avons travaillé avec un langage vraiment "bas-niveau", pourtant revoir le code a probablement semblé très familier à la plupart ce qui permet d'écrire du code à la fois ultra-rapide et efficace en mémoire sans la peur qui vient avec une telle responsabilité: on sais que le compilateur sera là, arrêtant le code avant même qu'il ne soit possible de l'exécuter.

Comme vous avez pu le voir, Rust est un langage de programmation qui est rapide, sécurisé et stable, son utilisation se répand de plus en plus chaque année et les possibilités qu'ils offrent sont énormes. Beaucoup vantent la qualité et l'entraide de la communauté ainsi que l'écosystème pratique et grandissant. C'est ce qui me motive à apprendre ce langage ainsi que la programmation. En espérant que j'aurais su vous motiver un peu plus ou du moins vous orienter.

N'hésitez pas à me contacter sur mon compte Twitter pour me donner votre feedback ou apporter quelques corrections.

Avant de finir, voici quelques ressources additionnelles pour vous aider à avancer dans votre aventure Rust:

En plus, je vous ajoute les liens vers les articles ou les ressources qui m'auront aidé à préparé ce livre au mieux: