4 points par GN⁺ 2025-08-20 | 1 commentaires | Partager sur WhatsApp
  • La programmation de gauche à droite permet au programme de rester valide dès la saisie du code, ce qui maximise la prise en charge par les outils comme l’autocomplétion de l’éditeur
  • Les list comprehensions de Python nuisent à l’autocomplétion à cause de variables non déclarées et de l’absence d’inférence de types
  • Rust et JavaScript permettent de construire naturellement le programme de gauche à droite, ce qui rend l’usage des variables et l’exploration des méthodes plus intuitifs
  • Le style fonctionnel en C et en Python nuit à une expérience de codage efficace à cause de la faible découvrabilité des noms de fonctions ou des structures
  • Pour les logiques complexes, un code qui se déploie de gauche à droite est plus facile à lire, avec une meilleure maintenabilité et extensibilité

Programmer de gauche à droite

Le code doit être valide dès le moment où on le saisit


Les limites des list comprehensions en Python

  • La syntaxe de list comprehension en Python words_on_lines = [line.split() for line in text.splitlines()] pose problème, car elle oblige à accéder à une variable non déclarée (line), ce qui empêche l’éditeur de fournir correctement l’autocomplétion ou l’inférence de types
  • Pendant la saisie partielle du code
    • si l’on tape words_on_lines = [line.sp, l’éditeur ne connaît pas le type de line et ne peut donc pas suggérer de méthodes
    • il devient aussi difficile de détecter des erreurs potentielles comme une faute de frappe dans le nom de variable (lime, par exemple)
  • Pour obtenir des suggestions correctes, il faut écrire du code inachevé, ce qui rend le processus peu intuitif et inconfortable

Construction de gauche à droite en Rust

  • Dans l’exemple Rust (let words_on_lines = text.lines().map(|line| line.split_whitespace());)
    • la variable (line) est considérée comme déclarée au moment même où elle apparaît pour la première fois avec la fonction anonyme, ce qui permet immédiatement l’autocomplétion et les suggestions de méthodes
    • en pratique, la méthode split_whitespace a elle aussi été facile à trouver grâce aux suggestions automatiques
  • Cette approche permet au programme de rester toujours partiellement valide, ce qui permet à l’IDE ou à l’éditeur d’aider à coder en temps réel

Progressive Disclosure et utilisabilité des API

  • La progressive disclosure est un principe de conception selon lequel l’utilisateur n’est exposé qu’au niveau de complexité dont il a besoin, et il peut aussi s’appliquer à la programmation
    • Exemple : une UX de traitement de texte où les options liées à une image n’apparaissent qu’au moment où l’on ajoute une image
  • Le langage C offre peu de support dans ce domaine
    • comme toutes les fonctions liées à FILE *file ne se découvrent pas via file., il faut mémoriser les conventions de nommage (fread, fclose, etc.), ce qui rend les fonctionnalités difficiles à découvrir
    • à l’inverse, dans un langage idéal, les suggestions de méthodes via file. permettraient de découvrir progressivement les fonctions associées

Différence de découvrabilité entre fonctions et méthodes

  • Comparaison entre les exemples Python map(len, text.split()) et JavaScript text.split(" ").map(word => word.length)
    • en Python, comme les noms de fonctions tels que len, length ou size ne sont pas prévisibles, il faut souvent essayer plusieurs variantes avant de trouver ce qui fonctionne réellement
    • en JavaScript, il suffit de taper .l après word. pour que l’éditeur propose length et d’autres méthodes, ce qui améliore fortement la découvrabilité
    • même des fonctions d’ordre supérieur comme map rendent immédiatement plus clairs la valeur de retour réelle et le type de données

Les avantages d’une écriture structurée quand la logique se complexifie

  • Dans les logiques complexes (long code Python avec filter et lambda imbriqués)
    • il faut vérifier à répétition le début et la fin du code, et les conditions ou parenthèses appariées entraînent une baisse de lisibilité et une compréhension plus difficile
  • Dans la version JavaScript de la même logique, on peut lire et comprendre le code de façon séquentielle, de haut en bas et de gauche à droite

Principe fondamental

Le code doit être valide à chaque instant de la saisie

  • Le programme reste valide même avec la seule saisie de text
  • Même après avoir écrit text.split(" "), puis en poursuivant avec .map(word => word.length), l’état intermédiaire reste toujours valide dans son ensemble
  • Ce type de codage augmente la possibilité d’un support en temps réel par l’éditeur et, dans un environnement REPL, permet aussi de voir immédiatement le résultat

Conclusion

  • Les API et le design des langages doivent permettre de saisir naturellement le code de gauche à droite, tout en créant à chaque étape intermédiaire un programme valide
  • Une bonne conception d’API est la clé de cette amélioration de l’expérience de programmation

1 commentaires

 
GN⁺ 2025-08-20
Commentaires Hacker News
  • L’un des défauts de SQL est que les requêtes commencent par SELECT et non par FROM, ce qui rend moins évident de voir immédiatement quelle entité (table) est concernée, et cela gêne aussi les éditeurs intelligents pour aider plus efficacement à écrire les requêtes ; aller dans l’ordre FROM -> SELECT -> WHERE paraît plus naturel, d’autant plus qu’on nomme les colonnes dans la clause SELECT puis qu’on les référence dans WHERE ; en fait, on pourrait même imaginer que la clause SELECT soit implicite et n’écrire que FROM table au lieu de SELECT * FROM table ; je sais que ce genre de plainte me fait passer pour un vieux grincheux, mais c’est juste une lubie personnelle
    • PSQL et PRQL utilisent effectivement un ordre de requête où FROM vient en premier ; BigQuery a aussi récemment ajouté une syntaxe pipe/flèche, et il existe des extensions communautaires pour DuckDB, donc je recommande d’y jeter un œil : DuckDB - PSQL, DuckDB - PRQL
    • Si SQL est écrit ainsi, c’est parce qu’en algèbre relationnelle on note traditionnellement d’abord la projection ; selon le standard, on ne peut donc pas utiliser un alias de colonne dans WHERE, car la sélection (WHERE) a lieu avant la projection (SELECT) ; à noter aussi que MySQL 8 propose une syntaxe TABLE <table> qui peut être intéressante
    • En pratique, l’ordre de traitement interne de la plupart des moteurs SQL est FROM -> WHERE -> SELECT ; c’est pourquoi les alias de colonnes définis dans SELECT peuvent être utilisés dans GROUP BY, HAVING et ORDER BY, mais pas dans WHERE
    • En C#, le DSL compilé vers SQL (LINQ-to-SQL) adopte lui aussi une structure où FROM vient d’abord ; et comme l’IDE peut proposer immédiatement des champs via l’autocomplétion quand on écrit les autres clauses, je trouve cette structure préférable
    • Kusto, le langage de requête d’analyse de données d’Azure, a aussi une forme proche basée sur des pipes : Introduction aux requêtes Kusto ; le style LINQ de .NET aussi ; franchement, SQL devrait adopter plus activement des variantes qui commencent par FROM, et je ne pense pas que ce soit difficile à faire ; il manque des efforts sur l’ergonomie
  • J’ai du mal à comprendre pourquoi Python est autant aimé ; dès que plus de deux personnes travaillent dessus, le langage devient une souffrance sans fond ; ce que l’auteur pointe n’est que la partie émergée de l’iceberg
    • Je pense que c’est un peu la même raison pour laquelle les gens ne se ruent pas sur les langages de la famille Lisp : la rigueur mathématique n’implique pas automatiquement la lisibilité ; les compréhensions de liste/dictionnaire/ensemble en Python sont du même ordre qu’une boucle for qui fixe le type ; tout le monde s’inquiète de la souplesse du typage Python, mais il est étrange que la seule syntaxe qui fixe clairement le type de retour — la compréhension de liste — soit prise pour cible ; dans la plupart des autres langages, Rust compris, l’ordre n’est pas non plus « from iter as var » ; et il est aussi amusant de comparer la syntaxe des appels de fonctions selon les langages (Python a aussi functools.map)
    • Ce n’est pas parce qu’on ne comprend pas quelque chose que cela devient une vertu ; s’il est autant apprécié, c’est bien qu’il y a une raison ; il a évidemment des défauts, mais cela ne suffit pas à dire grand-chose ; il faut comparer l’ensemble des avantages et inconvénients, et faire la même comparaison avec les autres langages
    • J’aime bien Python moi aussi (à condition de parler de petites équipes et de programmes courts à durée de vie limitée) ; l’absence de typage statique accélère l’implémentation, mais un système de types assez fort évite que tout parte complètement en vrille ; c’est sans doute pour cela qu’il est si populaire en data science ; il est très pratique pour l’exploration ; en revanche, il a clairement des inconvénients pour de gros programmes ou une maintenance de long terme par plusieurs équipes ; au final, il n’existe pas de langage universel, et il faut au minimum un langage « soft », avec lequel on peut essayer et avancer vite, et un langage « hard », mieux adapté à une gestion sur la durée
    • Avant, j’étais totalement d’accord avec ce point de vue, mais avec les annotations de type et la vérification de types, collaborer sur du code Python écrit par d’autres est devenu bien plus facile ; je ne pense toujours pas que ce soit idéal pour les très gros projets, mais avec les types, Python est devenu mon langage de script préféré
    • Dans une base de code partagée, j’évite moi aussi les choses comme les compréhensions de listes et je vise un style Python très simple ; on dit souvent que c’est un langage où « il ne devrait y avoir qu’une seule façon de faire », mais en réalité trop de façons coexistent ; les compréhensions de listes sont amusantes et satisfaisantes à titre personnel, mais si tout le monde doit suivre une seule voie, alors cette syntaxe ne devrait pas exister
  • Je comprends l’idée selon laquelle « un programme devrait être valide au fur et à mesure qu’on le tape », mais dans la réalité on n’écrit pas toujours le code strictement de gauche à droite, ligne par ligne ; on écrit souvent d’abord une autre partie, ou on déclare les variables plus tard ; par exemple, il arrive qu’on utilise une variable puis qu’on ne la déclare que bien plus loin
    • Comme un morceau de code est écrit une fois puis lu des dizaines ou des centaines de fois, je trouve qu’un code qui se lit séquentiellement est bien plus agréable qu’un code qui impose des sauts
    • Cette discussion s’éloigne un peu du cœur de l’article, mais c’est un point de vue intéressant
    • Entièrement d’accord ; je n’écris le code dans l’ordre strict du début à la fin que quand je crée un nouveau fichier ; quand j’ajoute un champ, je ne vais pas forcément d’abord à la définition de la classe, je commence plutôt directement par le code qui utilise ce champ ; et quand j’améliore une condition, il m’arrive souvent de passer temporairement par un état invalide (qui produit des erreurs)
    • Je suis aussi d’accord avec cela, mais le principe important qui y est lié, c’est qu’une structure qui « t’empêche carrément de compiler parce que tu n’as pas encore fini de coder » est excessive ; les erreurs devraient être non bloquantes, alors que certains langages bloquent complètement sur du code incomplet (par exemple variable inutilisée, return manquant, etc.)
    • Il m’arrive souvent de trouver l’IDE un peu frustrant quand j’ai l’impression qu’il ne comprend pas bien l’ordre réel dans lequel j’écris mon code
  • Dans certains IDE, les modèles de code permettent de taper une abréviation qui se développe en structure de code, puis de remplir chaque placeholder à l’aide de la touche Tab ; dans ce cas, l’ordre de tabulation n’a pas besoin d’être strictement de gauche à droite, donc on peut très bien avoir un ordre comme {3} for {2} in {1} ; ce genre d’outils offre un compromis entre une « syntaxe facile à lire » et une « syntaxe facile à taper » ; de mon côté, même si cela passe par de l’outillage, je préfère privilégier une syntaxe plus lisible ; il n’y a pas de raison de s’accrocher absolument à la structure « for-in »
  • Ces derniers temps, le consensus implicite sur Hacker News semble être que Python a raté l’opérateur pipe ; venant de Mathematica puis passé à R, j’ai vite compris la valeur du pipe ; pour écrire du code de transformation de données étape par étape en data science, c’est vraiment intuitif et lisible ; Python étant utilisé dans bien d’autres domaines que l’analyse de données, je me demande si le pipe aurait aussi des avantages dans d’autres contextes, et j’essaie de comprendre pourquoi Python ne l’a pas adopté
    • En allant un cran plus loin que l’opérateur pipe, je pense que l’affectation inversée pourrait aussi valoir le coup ; au lieu d’assigner un résultat à une variable avec quelque chose comme « let foo = ... », j’aimerais essayer une forme du type « ... =: foo »
    • L’opérateur pipe de R (surtout dans l’écosystème tidyverse) est pour moi la principale « killer app » ; je ne connais pas d’autre langage où le travail sur les données soit à ce point simple et agréable ; par exemple, au lieu d’imbriquer une recette de cookies comme bake(divide(add(knead(mix(flour, water, sugar, butter)),eggs),12),450,12), utiliser un pipe comme mix(flour, water, sugar, butter) %>% knead() %>% add(eggs) %>% divide(12) %>% bake(temp=450, minutes=12) est bien plus simple et lisible
    • Avec la syntaxe pipe de pandas en Python, on obtient ceci
      result = (df
       .pipe(fun1, arg1=1)
       .pipe(fun2, arg2=2)
      )
      
      En R, cela donne
      result <- df |>
       fun1(., arg1=1) |>
       fun2(., arg2=2)
      
      Les deux sont assez lisibles, mais l’avantage de R est que le pipe fonctionne mieux aussi en dehors des dataframes
  • Cette controverse est presque une guerre de religion, comme les débats FP (fonctionnel) vs OOP (orienté objet), ou vim contre emacs ; dans vim, l’opérateur vient d’abord, alors que dans emacs l’ordre de sélection vient avant ; les langages qui « se lisent comme l’anglais » ont en général une structure où le verbe vient d’abord (comme Lisp/Scheme), tandis que les langues où le verbe arrive à la fin, comme l’allemand ou le tamoul, se marient mieux avec un style OOP (le nom d’abord) ; par exemple, en tamoul on dira l’équivalent de « water drink », alors qu’en anglais c’est « drink water » ; cela peut expliquer pourquoi certaines personnes trouvent vim plus naturel ; ce n’est pas tant qu’un style soit meilleur qu’un autre : ils peuvent aussi être façonnés selon l’outil ou les préférences des gens, et de nos jours les modèles de langage permettent à peu près tout
    • À propos de « si l’on veut que cela se lise comme l’anglais, alors le verbe doit venir d’abord » : oui pour un langage impératif, mais dans un langage déclaratif, si l’on veut suivre l’anglais, on commencerait par le sujet
    • Sur l’idée que « l’allemand place toujours le verbe à la fin » : en réalité, dans les phrases simples, le verbe vient en deuxième position (I drink waterIch trinke Wasser), pas systématiquement tout à la fin
    • Concernant l’idée que l’opérateur vient d’abord dans vim, en réalité Kakoune fonctionne à l’inverse, et je trouve cette approche bien plus logique : Explication sur Kakoune
  • À l’inverse, la syntaxe Python from some_library import child_module est très intuitive ; en JS, une structure comme import { asYetUnknownModule } from SomeLibrary me paraît nettement moins intuitive
    • En JS, si l’on utilise un import de namespace comme ceci
      import * as someLibrary from "some-library"
      someLibrary.someFunction()
      
      l’autocomplétion de l’IDE fonctionne en pratique très bien, ce que je trouve appréciable : MDN namespace import
    • Je m’interroge sur cette obsession du mot-clé from ; on pourrait simplement écrire
      import SomeLibrary {
        asYetUnknownModule
      }
      
      et ce serait très bien
  • ReScript a justement modifié ses API de data-last vers data-first pour cette raison ; grâce à une excellente inférence de types, l’autocomplétion est presque toujours exacte et cohérente avec les types, ce qui rend l’expérience de développement très agréable ; bien sûr, si l’on déclare une fonction sans référence préalable (et donc sans connaître son type), le problème subsiste, mais il se règle en ajoutant des types ou en écrivant l’appel d’abord ; je recommande aussi ce billet : Comparaison entre data-first et data-last
  • C’est une idée que je défends depuis longtemps, et cela rejoint aussi le fait que Ruby m’a toujours semblé bien plus facile ; pourtant, je n’ai jamais utilisé Python ni Ruby très profondément en production, mais j’ai du mal à comprendre pourquoi Python a commencé à être installé et utilisé aussi largement ; Ruby n’est pas exempt de défauts non plus, mais peu de gens semblent avoir eu à affronter le genre d’évolutions complexes qu’a connues Python dans le scripting ; et au moins, Ruby n’a pas connu ces dix dernières années de gros épisodes de rupture de version
  • Globalement, je suis entièrement d’accord avec ce que souligne l’article : un contexte placé au début et une lecture de gauche à droite devraient mieux convenir aux LLM et à l’autocomplétion ; en revanche, au lieu d’écrire quelque chose comme len(list(filter(lambda line: all([abs(x) >= 1 and abs(x) <= 3 for x in line]) and (all([x > 0 for x in line]) or all([x < 0 for x in line])), diffs))), il vaut mieux utiliser un array NumPy, ce qui évite de créer inutilement de nouvelles listes en mémoire et permet de traiter toute la ligne d’un coup ; par exemple
    sum(1 for line in diffs
      if ((np.abs(line) >= 1) & (np.abs(line) <= 3)).all()
        and ((line > 0).all() or (line < 0).all()))
    
    reflète beaucoup mieux cette logique « de gauche à droite »
    • La version NumPy reste encore un peu ésotérique (même si line > 0 est acceptable, les règles de broadcasting peuvent devenir complexes), mais les API de collections dans des langages au typage plus strict, comme dans l’exemple JavaScript de l’auteur ou en C#, Java et Scala, sont plus propres ; personnellement, je préfère Kotlin, parce qu’on peut écrire ceci
      diffs.countIf { line -> 
        line.all { abs(it) in 1..3 } and ( 
          line.all { it > 0} or
          line.all { it < 0}
        )
      }