36 points par GN⁺ 2025-08-23 | 1 commentaires | Partager sur WhatsApp
  • 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, Result et 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 null pour é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
    • Result et Iterator sont impossibles à implémenter sans génériques
    • Les concepts Send/Sync et les contraintes de println ne peuvent être exprimés de manière sûre qu’avec les traits
    • Le borrow checker garantit la sûreté Send/Sync grâ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

 
GN⁺ 2025-08-23
Avis Hacker News
  • Je trouve ironique qu’il y ait des bugs même dans un programme JS « simple ». La documentation de fs.watch indique explicitement qu’il faut impérativement vérifier si filename peut être null dans 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ée
    • Avec TypeScript, la vérification de null est 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é Rust
    • Il y a aussi un bug supplémentaire : for path in paths devrait être for (const path of paths). En JS, l’absence de parenthèses provoque immédiatement une erreur, mais la différence entre in et of est que in parcourt 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 erreur
    • Comme cela a été signalé, la syntaxe de la boucle elle-même est incorrecte, et il suffit d’exécuter le code pour le voir. Mieux vaut donc considérer que l’auteur n’a simplement pas porté beaucoup d’attention à ce code JS et que cela n’apporte pas grand-chose au débat
    • Je l’ai peut-être raté, mais je me demande d’où vient kind. Dans console.log("${kind} ${filename}"), cela devrait être eventType (une chaîne), pas kind
  • Je voudrais faire une remarque mineure. En Rust, println ne peut afficher que des types qui implémentent les traits Display ou Debug. Un Path ne 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 un Path peut donc entraîner une perte d’information. Path renvoie via la méthode display un type qui implémente Display. 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 directement TextEncoder/TextDecoder. D’après une ancienne expérience, si un serveur envoie du texte en Shift_JIS et qu’on le lit avec response.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 faut for-of et non for-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 apprendre async/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 :

      var fn = async (param) => new Promise((res, rej) => {
        fooLibraryCall(param).then(res).catch(rej);
      });
      

      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

  • Je pense que la citation de Bjarne est en réalité un argument commercial destiné à justifier de façon répétée la dégradation progressive de C++. Au début, c’était peut-être sincère, mais maintenant le schéma se répète. Je le vois comme ceci :
    1. « Il y a à l’intérieur de C++ un langage plus petit et plus propre »
    2. Mais comme on ne peut pas extraire ce sous-ensemble du langage, on propose d’abord de créer un sur-ensemble (plus riche en fonctionnalités), puis de faire le sous-ensemble après
    3. Le sur-ensemble entre dans le nouveau C++N+1. On dit qu’on discutera du vrai sous-ensemble ensuite, puis on reporte et on recommence
    4. C++N+1 devient plus complexe, et cela se répète indéfiniment Je ne comprends pas pourquoi ceux qui ont déjà vu cela se reproduire restent encore. Au final, ce « langage plus petit et plus propre » n’arrive jamais. On recommence toujours à l’étape un
    • Ça me fait penser à xkcd 927. Le standard C++ devient toujours plus complexe à chaque nouvelle version ; il y a certes de bonnes évolutions, mais elles s’accordent mal avec les anciennes versions, et le code source devient de plus en plus chaotique. Je maintiens deux bibliothèques OSS, mais je n’utilise presque plus C++ maintenant. Je me demande ces temps-ci combien de temps je vais encore tenir. Rust, en venant de c++11/14/17/20, est une vraie bouffée d’air frais. Cela dit, Rust reste tout à fait vaste si on ne le connaît pas dans son ensemble. J’ai trouvé les remarques de cet article très justes
  • Y a-t-il quelqu’un d’autre qui a immédiatement été distrait en voyant le shebang (script rust exécutable directement) ? J’ai eu la même surprise que quand j’avais découvert la même chose avec Go. Ça a l’air assez utile et probablement suffisant pour des usages de base. J’ai déjà vu quelque chose de similaire dans des projets qui gèrent leurs pipelines de build/test en Rust. Pour ce type d’usage, cela pourrait être une alternative tout à fait correcte. Cela dit, dès que j’ai besoin d’un script qui dépasse un peu bash, j’utilise généralement Deno+TS. C’est JS que j’ai pratiqué le plus longtemps (28 ans), puis C# pendant 24 ans. J’utilise Node depuis ses débuts. Je trouve Deno plus simple à gérer que Node ou Python du point de vue du partage et de la centralisation des packages. Le frontmatter de cargo fonctionne de façon similaire
    • C’est moi qui ai directement conçu et implémenté l’intégration des scripts dans cargo (même s’il y a eu beaucoup d’implémentations tierces jusque-là). Je suis très heureux de voir des cas d’usage réels et de constater que c’est mentionné. Voir aussi la documentation. Il y a eu de longues discussions sur la forme appropriée, la façon de l’intégrer au langage et le périmètre de la première version. En ce moment, on termine notamment le guide de style et la mise à jour de la référence Rust ; les gros sujets restants concernent les détails autour de rustfmt, rust-analyzer, quelques correctifs de bugs dans rustc et l’amélioration des rapports d’erreur de Cargo. Moi-même, j’utilise tous les jours cargo script pour écrire des scripts de reproduction d’issues
    • En fait, c’est en commençant à chercher le mot-clé de fonctionnalité -Zscript et à 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
    • On m’a dit qu’on pouvait faire la même chose en Go ; j’aimerais bien qu’on me l’explique plus en détail. Si c’est ce lien, alors ça m’intéresse aussi
    • J’ai beau avoir longtemps utilisé JS et C#, je ne trouve pas que ce soit une bonne raison de choisir un système en 2025. Beaucoup, vraiment beaucoup de choses se sont nettement améliorées au cours des vingt dernières années
    • Ce n’est qu’une fonctionnalité Unix de base. Un fichier qui commence par #!/some/path est simplement exécuté par le shell en passant tout le fichier sur stdin à la commande indiquée
  • Quand on dit qu’« un langage plus petit et plus propre émerge à l’intérieur de Rust », je me demande très précisément de quel langage il s’agit. À lire l’article, il faudrait quand même garder les références, les durées de vie, les traits, les énumérations, etc. pour que ça fonctionne, et dans ce cas ce n’est guère différent de Rust. La dernière partie donne deux indices — le « Rust que je veux utiliser » et le « Rust du passé » — mais ça ne me parle pas beaucoup. J’ai aussi lu « Notes on a smaller Rust » de withoutboats, mais les objectifs de conception y sont différents de ceux de Rust : il ne s’agit pas de devenir Rust, mais plutôt de montrer les leçons qu’on peut tirer de Rust lorsqu’on conçoit un nouveau langage. Ce n’est pas un langage qui cherche à être Rust, mais un exemple de langage adapté à des exigences « mainstream » (par ex. GC, simplification de la compilation/de la syntaxe, etc.). Deuxièmement, il est aussi dit : « le langage dont je suis tombé amoureux quand j’ai commencé à apprendre Rust en 2018, c’était ce “Rust plus petit” », mais en réalité Rust n’a pas fondamentalement beaucoup changé depuis 2018. La plupart des changements, comme les editions, relèvent d’améliorations de souplesse syntaxique ; les seules grandes exceptions sont vraiment async et const. Il aurait donc suffi de dire explicitement que « Rust avant l’arrivée de async et const était plus petit et plus propre », et je trouve dommage que le texte ne l’exprime pas aussi directement
    • Si on parle de « Rust plus petit et plus propre », Austral me vient à l’esprit comme exemple
    • Certains soutiennent qu’un langage plus simple (plus petit) reste possible tout en conservant les concepts clés de Rust. Par exemple, on pourrait retirer le trait Copy, le reborrowing, la deref coercion, l’appel automatique à into_iter dans les boucles, l’appel automatique à drop en fin de portée (on pourrait aussi l’appeler explicitement ou laisser le compilateur signaler une erreur), le :Sized implicite par défaut dans les bounds de traits, l’élision des durées de vie (lifetime elision), l’ergonomie du match et 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ébutants
    • J’ai lu cela avec beaucoup d’attention. En réalité, mon intention était bien de dire que Rust, avant l’introduction de async et const, é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
  • Je peux avoir un biais, mais je pense que Rust est le langage le plus proche de la perfection. Le borrow checker est pénible, mais indispensable. Si le même code bugué existait en C, il aurait provoqué un effondrement à l’exécution — et il aurait de toute façon fallu corriger le bug. La différence, c’est que Rust oblige à le résoudre avant même la compilation, alors qu’en C on gère la catastrophe en pleine nuit. Rust n’est pas tant difficile qu’il n’exige un changement de manière de penser. Il faut un changement de paradigme vers l’écriture d’un code sûr et sécurisé. Le changement est le plus souvent inconfortable, et c’est sans doute la cause profonde du rejet de Rust
    • Rust est loin d’être parfait
      • Je trouve que le compilateur a trop de liberté pour décider quand et dans quel ordre appliquer le deref. .into() et le trait From rendent 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)
      • Le retour implicite masque le flux du programme et favorise les erreurs. Je ne suis pas non plus fan de l’opérateur point d’interrogation
      • Rust fragmente trop les petits modules, si bien qu’il faut des centaines de dépendances pour faire quelque chose d’utile. Comme chacune est gérée séparément et doit être vendoriée pour garantir des builds stables, c’est vraiment pénible
      • L’async Rust est actuellement un chaos total
    • Ce n’est pas tant le borrow checker lui-même qui pose problème, mais plutôt le fait que Rust est devenu un trop gros « bloc ». Pour ceux qui aimaient le Rust brut et un peu imparfait de 2018 (moi y compris), l’état actuel est moins attirant. Bien sûr, c’est extrêmement puissant quand on le maîtrise, mais on finit par se demander si cela vaut vraiment autant d’efforts. En 2025, je choisirais sans doute Zig comme alternative à C/C++ (la seule exception serait le travail sur Postgres, l’écosystème pgrx étant vraiment à part). Malgré tout, tout vaut mieux que travailler en C
  • Je ne pense pas qu’il faille recommander Rust comme premier langage. Apprendre un premier langage est déjà difficile, et avec Rust, à cause des erreurs du compilateur, il est souvent impossible de voir son code s’exécuter avant qu’il soit complètement correct. C’est trop frustrant, et beaucoup abandonneront facilement. Je conseillerais plutôt de commencer par Python, JavaScript ou Lua, de faire rapidement quelque chose comme un jeu, puis d’itérer
    • Mon expérience est différente. Dans notre entreprise, un ingénieur ML ne connaissait que Python, mais voulait contribuer à une codebase Rust ; je lui ai expliqué les bases pendant environ une heure, et il s’est adapté très vite, avec une productivité qui a immédiatement décollé. En fait, quand on crée un jeu et qu’il plante parce qu’on a passé une chaîne à une fonction numérique, on peut perdre énormément de temps à comprendre pourquoi. En Rust, le compilateur indique directement : « ici c’est une string, il faut un int ». Au lieu de passer la journée à corriger des erreurs de compilation, on ne passe pas une semaine à souffrir d’erreurs à l’exécution
    • C’est moi l’auteur de la citation en haut du billet. J’ai enseigné Rust comme premier langage à plus de 400 personnes, et je trouve les affirmations de ce fil très intéressantes. Avec une expérience directe sur une longue période, j’ai accumulé suffisamment d’éléments montrant non seulement que c’est possible, mais que cela fonctionne plutôt bien
    • Je ne suis toujours pas convaincu. J’aimerais voir un bon pédagogue tenter Rust comme premier langage. Les générations changent et les universités utilisent désormais beaucoup Python, mais en théorie, Rust pourrait comme premier langage élever le niveau de toute une cohorte (même si le taux d’échec pourrait être trop élevé et poser des problèmes administratifs ; à l’inverse, les meilleurs étudiants pourraient apprendre davantage). Des choses apprises en Rust, comme la move assignment ou le sens du mot-clé const, peuvent aussi éviter plus tard l’effort de désapprendre de mauvaises habitudes prises dans des langages plus anciens
    • En général, j’aurais plutôt tendance à déconseiller le typage statique comme premier langage. Moi aussi j’aime le typage statique, mais du point de vue d’un débutant cela ajoute souvent juste de la confusion. Les erreurs du compilateur sont souvent contrefactuelles, et un message du type « le compilateur n’a pas pu prouver que ce n’était pas none » 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 longtemps
    • Rust n’est pas un mauvais langage si l’on est capable d’absorber tout d’un coup. Le problème, c’est que personne n’apprend réellement un langage comme ça, et tant qu’on ne maîtrise pas suffisamment les concepts clés, on accumule les tâtonnements en Rust. Et comme il y a aussi beaucoup de concepts qu’on n’apprend dans aucun autre langage, on risque d’être de nouveau frustré en passant à un nouveau langage
  • Le cas le plus proche d’un « Rust simple » que j’ai vu est Gleam. Cela semble fortement inspiré de Rust
    • Dire que Gleam est inspiré par Rust est un malentendu. Son créateur ne l’a pas affirmé officiellement. Le compilateur est écrit en Rust, mais Gleam a un paradigme et des runtimes cibles complètement différents, donc ce n’est pas un substitut à Rust
    • Si vous cherchez un autre style de « Rust simple », je recommanderais aussi de jeter un œil à fsharp
    • La page principale de Gleam affiche des messages sur les droits des Noirs, les droits trans et l’anti-nazisme, donc ce langage ne m’intéresse absolument pas
    • Je me demande si on peut faire de la 3D avec Gleam
  • Le fait qu’on « n’explique pas ce que fait le programme Rust » m’a frappé. Il y a une énorme explication technique, mais aucun résumé de ce que fait réellement le programme. En réalité, il ne fait qu’observer les changements de fichiers et les afficher. Cela montre bien la difficulté du langage : même pour une tâche simple, l’implémentation en Rust se complexifie et il faut se soucier de détails internes sans rapport direct avec le problème réel. J’y vois à la fois la difficulté à laquelle on se confronte et une barrière que le langage se construit lui-même
    • Les autres langages ont exactement le même problème, mais Rust aide à le traiter en amont. Tous les noms de fichiers ne sont pas affichables, et la plupart des langages laissent simplement ce problème à l’utilisateur. Rust indique clairement les erreurs/échecs dans ses types de retour, alors que d’autres langages ont besoin de mécanismes différents, comme la gestion des exceptions. Même si cela paraît simple en apparence, Rust peut en réalité être plus intuitif
    • En fait, pour un langage haute performance, l’implémentation est très simple. Tout tient sur une page. Ce n’est pas suffisamment simple comme exemple ?
    • Explication simple = implémentation simple n’est pas toujours vrai. xkcd 1425 l’illustre bien. (Par ex. vérifier qu’une photo a été prise dans un parc national est facile, mais déterminer en plus s’il s’agit d’une photo d’oiseau demande une équipe de recherche)
  • Je trouve Rust assez cohérent et bien unifié sur le plan sémantique. Par rapport à d’autres langages, il y a moins de sucre syntaxique arbitraire, ce qui le rend plus intuitif. La plupart des interfaces suivent généralement les modèles du module mem, donc pour bien comprendre la structure des interfaces, mieux vaut commencer par std::mem