Sommaire
- Description du Rust
- Préparation du projet
- Gestion des paramètres
- La structure
App
- La macro
print_now
- Le main
- Conclusion
Comme promis, voici un petit journal sur mon port de taptempo en Rust
. Je n'ai pas porté le mécanisme d'internationalisation, puisque finalement on peut le faire comme dans le code C++
, avec gettext
. Ce n'est pas le plus intéressant du projet, et il n'existe pas de mécanisme d'internationalisation que je trouve vraiment satisfaisant et idiomatique pour le moment.
Description du Rust
Pour ceux qui ne connaissent pas le langage, la façon la plus simple de le décrire est de dire que c'est un croisement entre le C
et le Haskell
. Comme le C
, il est bas-niveau (on peut faire en Rust
ce qu'on ferait en C
: programmation de micro-contrôleurs, d'OS, etc.) mais il tire du Haskell
le fait d'être "orienté expression", sa saveur fonctionnelle, la richesse de son système de types.
Ce qui fait sa particularité et son immense avantage, c'est qu'il est memory safe et thread safe. Le langage garantit qu'il n'y a pas de pointeur invalide ou de "data race", le tout sans ramasse-miettes.
Préparation du projet
Un autre point est que comme beaucoup de langages modernes, le systèmes de dépendances (appelées crates en Rust
) est très simple. On rajoute une ligne dans le manifeste du projet (appelé Cargo.toml
) et le gestionnaire de build télécharge la dépendance et l'inclus dans le build. C'est tellement idiomatique et facile qu'il y a certaines dépendances qui n'ajoutent qu'une petite fonction ou macro utilitaire. Voici les outils que j'ai sélectionnés:
- Ce projet utilise les options de lignes de commande : le crate indispensable pour ceci est
StructOpt
qui ajoute des macros procédurales permettant de récupérer les paramètres entrés en ligne de commande très facilement. - Ensuite, j'ajoute un crate pour avoir une queue circulaire (puisque c'est la structure de données que nous avons en interne). En l’occurrence, je n'ai pas trouvé de crate satisfaisant, et donc j'ai écrit le mien (il n'est pas encore publié puisqu'il est encore en chantier, mais ça viendra ;) ).
Gestion des paramètres
J'ai ajouté une nouvelle structure que j'appelle de façon originale Params
. Vous verrez au-dessus la ligne suivante:
#[derive(Debug, StructOpt)]
qui permet de faire dériver notre struct
des trait
s Debug
et le fameux StructOpt
. Le trait Debug
devrait être implémenté sur toutes les structures de données puisqu'il permet de leur donner une représentation textuelle. Pratique pour le débug dans la console ou dans un log. Le trait StructOpt
va permettre la génération du code pour la gestion des paramètres. Quand on tape ce genre de lignes, en interne une macro est invoquée qui va parser l'AST de la struct
et en faire ce qu'on veut. Une macro procédurale est un générateur de code.
Du coup, je vais ajouter une donnée dans ma struct
:
precision: usize,
et au-dessus, je vais donner des informations à StructOpt
: le code court et long pour ajouter le paramètre (-p
et --precision
), la valeur par défaut, la documentation, etc. Tout est dans la doc du crate.
J'implémente ensuite un trait pour dire que notre structure a des valeurs par défaut. Je crée les données avec les infos passées en ligne de commande, et ensuite je modifie les chiffres en fonction des bornes.
Contrairement à Ada
, on ne peut pas faire de type numérique borné pour le moment. Le système de type n'est pas achevé (le langage est encore jeune), mais quand ça viendra, ce type existera ! D'ici fin 2018 ça devrait être fait.
La structure App
Notre structure principale va ressembler à l'objet C++
originel. J'implémente le trait pour donner une valeur par défaut comme pour StructOpt
(c'est vaguement un équivalent du constructeur par défaut en C++
).
Ensuite, je suppose que je n'ai pas besoin de décrire dans le détail toutes les fonctions dans App
, le nom est explicite et ça ressemble au code d'origine. Je vais juste aborder quelques spécificités du langage.
En Rust
, une opération qui échoue retourne un type Result
qui peut avoir deux valeurs: Ok
avec l'objet attendu ou Err
avec le type d'erreur qui s'est produite. Ça permet de gérer élégamment les erreurs sans mécanisme d'exceptions.
Le langage est "orienté expressions", donc on peut écrire des choses du genre :
fn must_continue(reader: &mut Lines<StdinLock>) -> Result<bool, IoError> {
match reader.next() {
None => Ok(false),
Some(r) => r.map(|s| s != "q")
}
}
Cette fonction doit se lire ainsi :
- Si on n'a pas de nouvelle ligne (l'utilisateur a tapé CTRL
+ D
), l'opération a réussi et on ne doit pas continuer: Ok(false)
.
- Si on a une ligne, on prend le résultat, et avec map
on transforme le Ok(String)
en Ok(bool)
en comparant la résultat avec "q"
.
La macro print_now
Comme dans la plupart des langages, la sortie standard est buffurisée et donc rien n'est affiché tant qu'il n'y a pas de retour à la ligne. Du coup, j'ai écrit une petite macro qui vide les buffers après avoir appelé print
.
Ça permet de voir le fonctionnement des macros (classiques cette fois-ci, en opposition aux macros procédurales dont j'ai parlé plus haut): contrairement aux macros du C
/C++
, celles du Rust
sont sémantiques, donc on doit faire matcher avec des choses qui ont du sens, et non pas "aveuglément". En l’occurrence, je récupère des expressions ($e:expr
) ; Le ,*
permet de dire qu'on veut en récupérer 0 ou plus séparées par des virgules.
La syntaxe peut sembler un peu ésotérique, mais le mécanisme est puissant et sécurisé.
Le main
Le main est simple, il va créer un objet de type App
et lancer la méthode run
puis vérifier le retour d'erreur.
Conclusion
J'espère que je vous ai donné envie de découvrir un peu plus ce langage qui est selon moi l'alternative la plus crédible au C
. C'est un sentiment incroyable de faire un développement bas-niveau et de savoir que si le code compile, on ne tombe pas sur des erreurs non prévues au runtime (du genre segfault). Ceci permet de faire des réusinages massifs sans crainte, puisque le compilateur sait si on a fait quelque chose d'invalide au niveau de la mémoire.
En tout cas, j'ai moi-même progressé avec ce projet, et je suis parti pour publier au moins 3 crates différents. Le Rust
est beaucoup basé sur le principe de l'ouverture du code (pour être publié en ligne, un crate doit être open-source) et ça fait toujours plaisir d'apporter sa pierre à l'édifice.
# Je viens de comprendre
Posté par groumf . Évalué à -3.
En fait TapTempo c'est le nouveau johnny.
[^] # Re: Je viens de comprendre
Posté par Cheuteumi . Évalué à 0.
Non, c’est le nouveau noir.
[^] # Re: Je viens de comprendre
Posté par Benoît Sibaud (site web personnel) . Évalué à 10.
Je pense lancer Metaptempo, qui prend un port au hasard et le lance, ou si l'option --all est utilisée lance tous les ports en parallèle pour comparer leur précision. Évidemment la création de ports de Metaptempo nécessiterait un Metataptempo. Voire un HakunaMetataptempo.
[^] # Re: Je viens de comprendre
Posté par groumf . Évalué à 1.
Merci Benoit pour ton dévouement à Linuxfr, on ne le dit pas assez.
[^] # Re: Je viens de comprendre
Posté par Cheuteumi . Évalué à 3.
Vu le nombre de portages en cours je pense qu’il serait avantageux de louer un ordinateur quantique afin de ne pas interférer dans l’exécution en parallèle, vu qu’on va manquer de cœurs :/
[^] # Re: Je viens de comprendre
Posté par Anonyme . Évalué à 3. Dernière modification le 04 mars 2018 à 16:12.
on trouve des serveurs ARM à 96 cœurs, il y a encore de la marge.
# Super !
Posté par Blackknight (site web personnel, Mastodon) . Évalué à 5.
Cool ! Je l'attendais avec impatience ce journal :)
Sur le plan technique, je reste toujours dubitatif sur la notation
avec son |s| qui donne l'impression d'une variable sortie ex nihilo mais je commence à comprendre un peu mieux suite au commentaire de Kantien… Même si c'est pas encore cristal clear :D
D'autre part, dans
Le fait que first et last ne soient pas déclarés avec un type me dérange toujours même si la ligne d'après me fait comprendre qu'il s'agit d'un pointeur vers un temps. Du coup, je trouve que ça limite la visibilité et que si on se retrouvait avec un tel code à l'autre bout du programme, ce serait plus difficile à comprendre.
Gloabalement, ce n'est pas non plus totalement incompréhensible pour un dino comme moi qui est resté bloqué sur du procédural/objet :D
Encore merci
[^] # Re: Super !
Posté par j_m . Évalué à 5. Dernière modification le 04 mars 2018 à 09:53.
J'imagine qu'on a le choix d'ajouter des types pour aider la lecture, meme si le compilateur n'en a pas besoin pour inferer les types correctement. Comme ici en Scala:
[^] # Re: Super !
Posté par j_m . Évalué à 3.
Finalement, j'ai un peu cherche et voila le meme exemple en Rust (que vous pouvez executer en ligne ici: https://play.rust-lang.org/), c'est un peu plus verbeux que le Scala ^_^ :
Pour ceux qui ne sont pas habitue a l'inference de type, le typage dans
est facultatif et si vous vous trompez le compilateur vous corrige. i8 a la place de i16 ne compile pas par exemple. C'est vraiment tres pratique.
[^] # Re: Super !
Posté par Morovaille . Évalué à 2.
Pour la traduction c'est presque ça. On n'a pas besoin du
return
dans la fonctionf
. On écrira plutôt:Sinon, oui, c'est plus verbeux que
Scala
,Ocaml
,Haskell
, etc. puisque les concepteurs du langage ont fait le choix d'utiliser une syntaxeC
-esque.Ce commentaire est libre de droit, vous pouvez le réutiliser comme bon vous semble.
[^] # Re: Super !
Posté par Thomas Douillard . Évalué à 4.
Haskell, avec la notion de foncteur, a généralisé cette idée avec la notion de « foncteur » et fmap : un « truc » qui transforme une fonction de A -> B en une fonction « Truc A -> Truc B » est le fmap du foncteur truc ( https://hackage.haskell.org/package/base-4.10.1.0/docs/Control-Monad.html#v:fmap )
Corrigez moi si je me trompe mais en l’occurrence « map » est le « fmap » du std::option de rust : il permet d’appliquer une fonction A -> B qui n’a pas à se soucier de si son A peut être null ou que sais-je en une fonction « option A -> option B » (« Truc A -> Truc B » avec Truc = option) qui va gérer d’elle même les cas « null » dans d’autres langage.
Et effectivement, on peux trouver son code ici : https://doc.rust-lang.org/src/core/option.rs.html#402-407
f est une fonction qui renvoie un U et map(f) renvoie un option.
L’idée du « Maybe » en haskell c’est que tu écris tes fonctions sans te soucier des cas d’erreurs ou des éventuels « null » quand rien ne peut arriver, comme si tu écrivais du code avec exceptions presque (ou des fonctions qui prennent un A et retournent un « peut être B » ( voir https://en.wikibooks.org/wiki/Haskell/Understanding_monads#Motivation:_Maybe ). Et le langage t’aides à les composer comme il faut avec des opérateurs et du sucre syntaxique éventuellement. C’est un cas particulier de « monade ». Dans un monade des opérations sont abstraites de la même manière que « fmap » est abstraite dans le début de mon commentaire.
[^] # Re: Super !
Posté par Morovaille . Évalué à 1.
J'ai oublié de préciser que
|s| s != q
est une closure, c'est-à-dire une fonction anonyme. La syntaxe est un peu étrange, mais les paramètres de la fonction se mettent entre deux barres verticales.Sinon, le compilateur est capable de tout inférer, donc on n'a pas besoin d'écrire les types de variable. Mais comme l'a dit un autre commentaire, on peut toujours écrire les types si on considère que le code sera plus clair. En l'occurrence, ça me semble plutôt intuitif que
front
etback
retournent une référence sur le début et la fin de la queue. De façon générale (comme dans les autres langages fonctionnels commeHaskell
ouOcaml
), on écrit de petites fonctions et on n'annote les variable que dans les (rares) cas où le compilateur a des problèmes d'inférence (typiquement, quand seul le retour d'une fonction est générique).Pour rebondir sur le dernier commentaire, il ne faut pas avoir peur du Rust : finalement ça ressemble quand même beaucoup aux langages objets : on a des structures sur lesquelles on implémente des méthodes ; ce qu'on peut voir comme une classe. En revanche, je ne suis pas spécialiste de l'histoire de l'informatique, mais il me semble que les langages fonctionnels sont plus vieux que les langages objets (en tout cas suffisamment vieux pour que n'importe quel dino connaisse :p). Le Lisp existe depuis 1958 selon Wikipédia.
Ce commentaire est libre de droit, vous pouvez le réutiliser comme bon vous semble.
# Dépendances
Posté par Glandos . Évalué à 6.
Oui, ça je comprends que ça ressemble à NPM. Mais ça m'inquiète en fait. Et c'est assez bien analysé par Lars Wirzenius.
Donc est-ce que mes craintes sont fondées ? Est-ce qu'on va finir avec des applications à moitié libres, parce que l'autre moitié sera composée de centaines de dépendances aux licences incompatibles ou au code source manquant ?
Ça ressemble à un troll, mais j'aime énormément le concept de Rust, et je n'aimerais pas que son écosystème me dissuade de l'utiliser :(
[^] # Re: Dépendances
Posté par Morovaille . Évalué à 2.
Je pense que la comparaison est bonne, oui, ça ressemble à NPM dans le sens où on a plein de petits paquets interdépendants. En fait, c'est même considéré comme idiomatique de découper les grosses libs en crates plus petits, voir par exemple le graphe des dépendances de
piston_window
qui est un crate pour développer des jeux vidéos.Il existe cependant quelques différences importantes :
- La bibliothèque de crates, crates.io, est officielle et maintenue par la communauté Rust, tandis que NPM n'est qu'un gestionnaire de paquets parmi d'autres, et n'est pas lié à ECMAScript.
- On ne peut pas modifier ou supprimer un crate publié sous une version. Si je publie mon crate
foobar
dans sa version0.1
, je ne peut plus le modifier ou le supprimer. Si je veux changer mon crate, je dois publier une version0.2
, sachant que la version0.1
sera toujours disponible pour la rétrocompatibilité.L'autre problème est celui des licences, et effectivement, ça peut être assez labyrinthique. C'est à chaque développeur de faire attention à ce qu'il utilise et à ne pas violer de licence. Mais finalement, le problème est le même quelle que soit la façon de procéder. Quand on inclut une bibliothèque dans un projet
C++
, on doit aussi faire attention à la licence, même si le téléchargement et la compilation de celle-ci ne se fait pas automatiquement à travers l'outil de build.Ce commentaire est libre de droit, vous pouvez le réutiliser comme bon vous semble.
[^] # Re: Dépendances
Posté par Glandos . Évalué à 2.
Disons qu'en C ou C++, atteindre plus d'une vingtaine de dépendances, c'est vraiment pas souvent. C'est vrai que c'est peut-être un frein au développement. Mais ça rend les choses un peu plus claires aussi.
En bref : ça manque d'outil d'analyse de dépendances ;)
[^] # Re: Dépendances
Posté par Morovaille . Évalué à 4.
Cet outil existe en Rust, il s'appelle
cargo-tree
:Ce commentaire est libre de droit, vous pouvez le réutiliser comme bon vous semble.
Suivre le flux des commentaires
Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.