Le cœur de Rust
(jyn.dev)- Rust est un langage dont les différents concepts sont étroitement imbriqués, si bien qu’il faut apprendre de nombreux éléments en même temps, même pour comprendre un programme de base
- Les fonctions, les génériques, les énumérations, le pattern matching, les traits, les références, l’ownership,
Send/Sync,Iterator, etc. sont tous des éléments fondamentaux conçus pour interagir entre eux - Comparé à JavaScript, avec JS il est possible d’écrire du code en ne connaissant qu’une partie des concepts, alors qu’avec Rust, il faut comprendre le contexte global du langage pour pouvoir écrire un code réellement pertinent
- Cette complexité propre à Rust augmente la difficulté d’apprentissage, mais apporte en contrepartie sécurité et cohérence, tout en influençant fortement la manière de concevoir le code
- Cette architecture linguistique fait la singularité de Rust, et la vision d’un « Rust plus petit » invite à reconsidérer une philosophie du langage finement articulée
La difficulté d’apprentissage de Rust
- Malgré une barrière à l’entrée élevée, de nombreuses personnes ont contribué à améliorer la documentation, les API et les diagnostics
- Parmi les concepts de base figurent les fonctions comme objets de première classe, les énumérations, le pattern matching, les génériques, les traits, les références, le borrow checker, la sûreté concurrente et les itérateurs
- Ces concepts sont interdépendants et imbriqués, ce qui rend difficile leur apprentissage isolé, et la bibliothèque standard exploite elle aussi largement ces fonctionnalités
- Même pour comprendre une vingtaine de lignes de code Rust, il faut souvent appréhender simultanément plusieurs éléments comme le paradigme fonctionnel,
Resultet la gestion des erreurs, les types génériques, les énumérations et les itérateurs
Comparaison entre Rust et JavaScript
- En écrivant le même programme de détection de changements de fichiers en Rust et en JS, on constate qu’en Rust de nombreux concepts du langage s’entrecroisent
- En JS, il suffit globalement de comprendre les fonctions et la gestion de
nullpour écrire du code fonctionnel - Cela ne signifie pas simplement que Rust est plus difficile, mais montre que Rust est conçu pour exiger une compréhension structurelle de l’ensemble du langage
La conception étroitement couplée de Rust
- Le cœur de Rust réside dans la combinaison de fonctionnalités conçues de manière organique
- Les énumérations sont peu pratiques sans pattern matching, et le pattern matching est lui aussi limité sans énumérations
ResultetIteratorsont impossibles à implémenter sans génériques- Les concepts
Send/Syncet les contraintes deprintlnne peuvent être exprimés de manière sûre qu’avec les traits - Le borrow checker garantit la sûreté
Send/Syncgrâce à l’analyse des captures de closures
- Cette forte interconnexion fait de Rust non pas un simple assemblage de fonctionnalités, mais un système de langage intégré
La vision d’un Rust plus petit
- En 2019, without.boats a évoqué « Smaller Rust » pour discuter de la possibilité d’un Rust plus petit et plus épuré
- Aujourd’hui, Rust est devenu bien plus vaste, mais l’idée d’un Rust plus petit rappelle l’essence d’une conception de langage finement imbriquée
- L’attrait de Rust tient au fait que ses éléments de langage, à la fois indépendants et combinables, offrent ensemble une forte expressivité et une grande sécurité
Conclusion
- Rust est difficile à apprendre, mais la cohérence et l’intégration de ses concepts entrelacés constituent l’un de ses plus grands atouts
- Grâce à cette structure, Rust amène les développeurs non seulement à écrire du code, mais aussi à adopter une manière de penser qui prend simultanément en compte sécurité et performance
- L’essence de Rust réside dans un « petit langage central raffiné », et cette philosophie reste importante même dans le Rust élargi d’aujourd’hui
1 commentaires
Avis Hacker News
fs.watchindique explicitement qu’il faut impérativement vérifier sifilenamepeut êtrenulldans le callback. En Rust, ce fait serait reflété dans le système de types et forcerait son traitement, alors qu’en JS il est facile d’écrire du code à la va-vite. Documentation associéenullest imposée. Je trouve que c’est un bon exemple montrant que TS est une étape relativement peu coûteuse qui rapproche JS de la correction qu’on retrouve côté Rustfor path in pathsdevrait êtrefor (const path of paths). En JS, l’absence de parenthèses provoque immédiatement une erreur, mais la différence entreinetofest queinparcourt les indices de l’itérable, pas les valeurs ; en pratique, l’indice est donc converti en chaîne et passé comme premier argument àfs.watch. Même TypeScript peut ne pas détecter cette erreurkind. Dansconsole.log("${kind} ${filename}"), cela devrait êtreeventType(une chaîne), paskindprintlnne peut afficher que des types qui implémentent les traitsDisplayouDebug. UnPathne peut donc pas être affiché directement. Tous les OS ne stockent pas leurs chemins dans un format compatible UTF-8, alors que tous les types chaîne de Rust sont en UTF-8. Afficher unPathpeut donc entraîner une perte d’information.Pathrenvoie via la méthodedisplayun type qui implémenteDisplay. Rust intègre cela dans son système de types, mais en JS/TS il est difficile d’exprimer explicitement que les chaînes internes sont en UTF-16, et pour manipuler correctement des chemins non Unicode il faut utiliser directementTextEncoder/TextDecoder. D’après une ancienne expérience, si un serveur envoie du texte en Shift_JIS et qu’on le lit avecresponse.text(), on peut se retrouver avec une chaîne vide à l’exécution. Si on n’est pas habitué aux problèmes d’encodage, on peut passer plusieurs jours à déboguer ce genre de situation. Et l’exemple JS contient des bugs et erreurs de syntaxe absents du code Rust (dans la boucle, il fautfor-ofet nonfor-in). On ne peut pas vraiment dire que cet exemple n’utilise que des « fonctions de première classe » : il faut aussi comprendre les itérateurs comme en Rust, et il utilise CommonJS. Il faut également apprendreasync/await, les Promises et le top-level await, et ce dernier n’a été pris en charge que récemment dans certains runtimes, y compris Node. Il n’est toujours pas pris en charge par certains moteurs JS (par ex. Hermes de React Native)C’est pour ce genre de raisons que je continue à utiliser Rust. Cet exemple n’en est qu’un parmi d’autres, mais ces petits problèmes et pièges sont partout dans les autres langages. Pris séparément, ils ne se produisent pas forcément, mais sur l’ensemble du cycle de vie d’un programme, ils s’accumulent et on se retrouve sans cesse avec des bugs bizarres qui surgissent de nulle part et qu’il faut traquer. En Rust, cela n’arrive pas. Le système de types élimine à l’avance un nombre absurde de cas problématiques. En pratique, une fois qu’on a livré un logiciel entièrement développé en Rust, on n’ajoute plus que des fonctionnalités de temps en temps et l’effort classique de chasse aux bugs disparaît presque. Bien sûr, des bugs logiques peuvent toujours exister, mais comme Rust empêche à la source les problèmes idiots issus d’incohérences de type ou de structure, la productivité et la maintenance deviennent une expérience totalement différente
Personnellement, j’ai l’impression qu’il n’y a pas tant de développeurs JS/TS qui maîtrisent vraiment les thenables/Promises et
async-await. J’ai déjà vu ce genre de chose :On emballe tel quel un wrapper basé sur des callbacks dans une Promise, puis on le réutilise encore dans une fonction async. À chaque fois, ça me fait mal au cœur. J’ai vraiment vu ce code un peu partout. Et si on ajoute à cela les imports de modules,
import()asynchrone, la transpilation, le code splitting, etc., ça devient vraiment complexe-Zscriptet à faire des recherches que je me suis laissé distraire. C’est en cours depuis 2023, et il y a aussi des issues ouvertes qui semblent proches d’être terminées. J’ai également vu dans le dépôt ZomboDB qu’ils géraient leur pipeline de build en Rust, mais je n’en ai pas complètement compris tout le contexte. Je voulais mentionner à quel point le frontmatter cargo est formidable pour la portabilité des scripts. Il suffit de partager un seul fichier, et on peut récupérer et utiliser les dépendances immédiatement, sans installation ni initialisation supplémentaires comme avec Python ou Node.js#!/some/pathest simplement exécuté par le shell en passant tout le fichier sur stdin à la commande indiquéeasyncetconst. Il aurait donc suffi de dire explicitement que « Rust avant l’arrivée deasyncetconstétait plus petit et plus propre », et je trouve dommage que le texte ne l’exprime pas aussi directementCopy, le reborrowing, la deref coercion, l’appel automatique àinto_iterdans les boucles, l’appel automatique àdropen fin de portée (on pourrait aussi l’appeler explicitement ou laisser le compilateur signaler une erreur), le:Sizedimplicite par défaut dans les bounds de traits, l’élision des durées de vie (lifetime elision), l’ergonomie dumatchet d’autres automatisations/conforts du même genre, et obtenir un Rust vraiment plus simple de manière mécanique. Mais un tel langage serait très inconfortable au quotidien. L’ironie, c’est que ces éléments ont justement été conçus pour les débutantsasyncetconst, était plus petit et plus propre. Si je ne l’ai pas formulé plus directement, c’est parce que j’ai beaucoup d’amis parmi les développeurs de ces fonctionnalités. Matklad l’exprime très bien sur lobste.rs. Le Rust de 2015 était plus abouti et plus cohérent, mais la vision de Rust n’est pas la cohérence totale (coherence) ; c’est de devenir un langage utile dans l’industrie.into()et le traitFromrendent les conversions de types trop implicites. Il y a beaucoup de fonctions de « confort » de ce genre aussi dans la bibliothèque standard. Au final, le type réel des objets devient ambigu, et il est difficile de relier les appels de fonction à leurs implémentations (même si un IDE aide un peu)const, peuvent aussi éviter plus tard l’effort de désapprendre de mauvaises habitudes prises dans des langages plus anciensnone» paraît bien plus difficile qu’un crash à l’exécution localisé directement par un cas de test. En affichant soi-même les valeurs ligne par ligne pour dépanner, on peut le plus souvent résoudre le problème assez vite ; alors qu’en se heurtant à des erreurs ésotériques du compilateur, on peut vraiment errer longtempsmem, donc pour bien comprendre la structure des interfaces, mieux vaut commencer par std::mem