Jouons au Rust 2 – Simple fraction to mixed number

Bon, celui-là a été plus long que prévu (et pour être honnête, je n’ai pas encore fini).

L’énoncé du problème est sur Codingame et, franchement, ça paraît assez vu assez simple de transformer 7/3 en 2 1/3.

Mais en fait, c’est un peu plus compliqué quand y réfléchit un peu :

  • D’abord, il faut séparer la partie entière de la fraction. C’est assez simple puisqu’il suffit de retirer le dénominateur au numérateur tant que le numérateur est plus grand (en partie entière, évidement).
  • Ensuite, il faut réduire la fraction. Et là, il y a du défi, puisqu’il faut séparer le numérateur et le dénominateur en nombres premiers, puis supprimer les nombres premiers trouvés dans le numérateur et le dénominateur.
  • Et enfin, il faut afficher le résultat

Ah, et j’avais oublié le fait de créer la fraction depuis une chaîne de caractères.

Lire une chaîne dans une struct

Là-dessus, Rust by Example explique clairement comment transformer une chaîne en un objet (une fraction dans mon cas). Et je dois dire que cette idée d’interfaces From et Into est particulièrement séduisante (d’autant qu’elle est facilement réplicable dans d’autres langages). Donc j’ai écrit une implémentation de impl From for Fraction, et ça a roulé, j’avais mon objet Fraction !

Séparer la partie entière de la fraction

Comme dans n’importe quel langage, les opérations sont faciles lorsque le modèle est bien aligné avec le langage. Et là, ma struct Fraction est … bien compatible avec Rust :

#[derive(Debug, Copy, Clone)]
struct Fraction {
  pub signum:i32,
  pub integer_part:i32,
  pub numerator:i32,
  pub denominator:i32
}

Ca n’a l’air de rien, mais pour la séparation de la partie entière, le code est raisonnablement trivial :

let mut next_numerator = self.numerator.abs();
let mut next_denominator = self.denominator.abs();
let mut next_integer_part = 0;
while next_denominator<=next_numerator {
  next_numerator = next_numerator-next_denominator;
  next_integer_part = next_integer_part+1;
}
return Fraction {
  signum:signum,
  integer_part:next_integer_part,
  numerator:next_numerator,
  denominator:next_denominator
}

Rien de sorcier, sauf que ça va fortement se compliquer dans un instant de toutes les façons possibles …

Réduire la fraction

Alors là, il nous faut les facteurs premiers du numérateur et du dénominateur. Et pour ça, il me fallait les nombres premiers.

Trouver les nombres premiers

Heureusement, j’ai l’habitude de calculer les nombres premiers, et je connais la méthode du crible d’Erastothène suffisamment bien pour l’implémenter sans frémir. Mais comme chaque test demande de convertir plusieurs fractions de taille inconnue, je voulais un crible extensible. J’avais donc initialement écrit un crible dans une structure statique

struct Primes {
 // Contain all primes up to a given value built from the primes_upto fonction
 PRIMES:Vec,
 // Contain for each number upto the given value the potential for this number to be a prime
 NUMBERS:Vec
}

static PRIMES = Primes::new()

Parce que je croyais follement que static avait le même sens qu’en Java. Alors que ça n’est pas le cas du tout. J’ai donc du enlever le static et passer mon objet Primes dans mon main. Pas grave, mais un peu verbeux. Ce qui était plus grave, c’est que Vec n’implémente pas Copy. Autrement dit, je ne passe pas mon objet Primesen paramètre d’une fonction parce que lors de l’appel, il y a semble-t-il une copie de la variable qui est faite (en terme de cycle de vie Rust). Et ça, j’ai mis un moment à le comprendre … Bref, c’est à ce moment que j’ai découvert std::cell::Cell. Cette classe un peu magique permet d’encapsuler le Vec pour qu’il ne « bouge » pas mais puisse quand même être une variable dans une struct. Exactement le genre de construction idiomatique pour laquelle je fais ces problèmes !

J’avais donc ma structure de nombres premiers

struct Primes {
  // Contain all primes up to a given value built from the primes_upto fonction
  PRIMES:Cell,
  // Contain for each number upto the given value the potential for this number to be a prime
  NUMBERS:Cell
}

Et l’ensemble des méthodes afférentes. Je laisse de côté la plupart des problèmes d’ownership et de borrowing parce que le compilateur Rust fait un travail exceptionnel d’explication des erreurs, avec même des suggestions de correction. J’adore ça.

Je pose juste la syntaxe pour obtenir le dernier élément d’un Vec : CURRENT_PRIMES.as_slice().last().unwrap_or(&2i32);. Le last() retourne une Option, et donc le unwrap_or(...) évite que le None ne pète une erreur, en remplaçant la valeur incorrecte.

Décomposer en facteurs premiers

Là aussi c’est assez simple : pour décomposer un nombre en facteurs premiers, on prend les facteurs premiers de 2 jusqu’à notre nombre, et on divise le nombre par tous ses diviseurs jusqu’à ce qu’il soit réduit à 1.

Pour ça, je prend donc mes nombres premiers, et j’itère dessus avec un bon vieil Iter (que j’ai obtenu facilement de mon Vec avec vec.iter()). Seulement, en Rust, un Iter retourne des Option. Donc le pattern d’itération classique de Java

while (iterator.hasNext()) {  value = iterator.next();}

devient quelque chose du genre

loop {  let value = iterator.next();
  match value {
    Some(v) => ...
    None => return ...
  }
}

(et encore je ne suis pas sûr de la syntaxe) Ca change !

Cela dit, avec ça, j’arrive à obtenir ma liste de facteurs premiers, et je peux réduire ma fraction (à quelques bugs près).

Afficher le résultat

Comme au début, le trait ToString permet de transformer ma structure en chaîne de caractère comme je veux. Par contre, je ne sais pas encore combiner ça avec le trait Display, qui me permettrait d’utiliser les macros d’affichage.

Et ensuite ?

J’ai encore quelques bugs pénibles à résoudre, qui vont me forcer à débugger mon code Rust (dans Eclipse ou VS Code, on verra) et à utiliser les outils de test intégrés dans Cargo.

En fait, tout ça fait de ce problème un truc super cool qui m’a permiss d’apprendre énormément de choses, et de vérifier tout un tas d’hypothèses, alors qu’il s’agit d’un problème résolu de façon habituelle par des élèves de 4ème (mon fils fait en ce moment des exercices semblables, et quand je lui ai dit que je galérais, il m’a regardé d’un air affligé)

Publicités

Jouons au Rust 1 – Porcupine Fever

Comme je l’écrivais hier, Codingame est un très bon moyen pour se plonger dans un langage. Et comme en ce moment je découvre Rust, faire quelques puzzles me paraît une bonne façon d’améliorer ma compréhension du truc.

Donc, pour commencer, un puzzle simple (voire très simple). Porcupine Fever est une bête histoire de map, que j’aurais implémenté sans trop y réfléchir en Java. Mais en Rust, il y a plusieurs points qui méritent une certaine attention.

J’ai à peu près tout indiqué dans les commentaires du code, et je vous encourage à suivre les liens. (au fait, le code complet est sur mon repo GitHub)

Il y a donc d’abord cette histoire d’itération. Effectivement, ça change pas mal des patterns Java. Mais franchement, je pense pouvoir m’y habituer assez vite.

Ensuite, et surtout, il y a cette sale histoire de structure qui doit dériver Comy et Clone. Pourquoi ? je ne comprend pas encore (mais ça viendra). Toujours est-il que je m’en souvienne, parce que le message d’erreur du compilateur Rust dit

error[E0507]: cannot move out of borrowed content
  --> /tmp/Answer.rs:53:61
   |
53 |         let mut next_cages:Vec = cages.iter().map(|c| c.derive()).collect();
   |                                                             ^ cannot move out of borrowed content

En fait, quand j’ai vu ce message d’erreur, j’ai commencé à ajouter des & ou des * au pif, mais évidement ça ne pouvait pas marcher. Parce que ça n’était pas la bonne erreur. C’est juste dommage que le compilateur ne soit pas dramatiquement intelligent pour me dire

hé, mec, tu utilises une struct dans un map(...), il faut que la struct implémente Copy et Clone !

Mais là, je crois que je ferais plus gaffe la prochaine fois.

Par contre si un bon « rustiste » ou « rustacé » peut m’expliquer pourquoi il faut que la struct implémente Copy pour que je puisse l’appeler dans mon map(...).

Est-ce que je suis rouillé ?

Le titre est « à double sens ».

Parce que je suis peut-être rouillé parce que ça fait un moment que je n’ai rien codé.

Mais aussi parce que, vous l’avez peut-être vu sur Twitter ou ailleurs, je commence à regarder sérieusement Rust. Et pour l’instant, j’y ai vu des choses différentes, mais rien d’aussi choquant que ce qu’a pu m’inspirer Go.

Bon, je dois bien reconnaître que je suis seulement en train d’attaquer la partie sur l’ownership et le borrowing. Les noms sont un peu barbares, mais il s’agit en fait du mécanisme de gestion de références de Rust, qui est censé éviter les memory leaks, parce qu’il force le développeur à gérer sérieusement ses objets.

Et puis … Il y a la doc. Aussi bien la doc de référence que le Rust by example sont vraiment bien fichus, progressifs, clairs, d’un niveau qui me fait penser la doc de Java un peu … frustre.

Et puis il y a cargo, qui est le package manager développé apparemment par la même équipe, et qui fournit tous les services d’un maven.

Et puis si Rust n’est pas vraiment orienté objet, il en a quand même un sacré paquet d’attributs, en plus des aspects fonctionnels qu’on trouve maintenant dans tous les langages.

Et puis il y a la non-facilité d’accès. Rust n’est pas tordu comme un C++. Mais il n’est pas non plus simple comme un Javascript. Et là, après mon mois de lecture de la doc, je me sens à peine capable d’aborder les problèmes simples de Codingame. Et c’est ce que je vais faire, parce qu’à mon avis, pour un langage performant comme Rust, c’est vraiment le terrain d’essai idéal.

En vrai, il y a une approche peut-être un peu sélective dans ce langage, mais je sens venir avec toute une série de gratifications en termes de sûreté de fonctionnement. Et ça, ça me fait plaisir. Et vous allez donc en lire un moment.

Utiliser PlantUML dans la preview Asciidoc de VS Code

Oui, parce que vendredi, j’ai craqué.

Malgré la haine persistante que j’ai pour l’idée d’installer un navigateur web et un serveur Node.js pour éditer des fichiers textes, j’ai enfin installé l’un de ces « nouveaux éditeurs de texte ».

Et comme Windows, c’était mon idée, j’ai choisi le pire de la world company : Visual Studio Code. Dans l’ensemble, c’est pas trop mal. Mais parfois, ça surprend. Par exemple, il y a une chouette extension pour faire de l’asciidoc dans Code. Mais elle utilise asciidoctor-js et ne supporte donc pas asciidoctor-diagram. Comment changer ça ?

Facilement (ou presque).

D’abord, installez ruby et asciidoctor-diagram (il installera automatiquement asciidoc).

Ensuite, dans vos paramètres de Visual Studio Code, ajoutez (merci à ce ticket)

// make sure Asciidoc extension shows diagrams !
"AsciiDoc.use_asciidoctor_js": false,
"AsciiDoc.asciidoctor_command": "asciidoctor -r asciidoctor-diagram -o-",

Redémarrez votre VS Code

Et vous pourrez mettre du PlantUML dans vos fichier asciidoc et avoir des previews comme ça.

2018-05-14 11_24_54-● configuration.adoc - Sans titre(Espace de travail) - Visual Studio Code

Par contre, vous n’aurez pas de coloration syntaxique du contenu PlantUML, bien qu’il existe également une extension VS Code pour ça. Et là, à mon avis, en utilisant un include asciidoc, on doit pouvoir marier le meilleur des deux mondes …

Devoxxfr – java.lang.Invoke

Finir Devoxx avec Rémi, c’est toujours cool.

On va s’intéresser aux loggers quand ils ne loggent pas. Et plus précisément quel est le coût d’exécution d’un logger qui ne logge pas ? Avec JMH, on a le résultat : ça prend 1 ns avec log4j2.

Et avec une lambda ? Ou un logger.isDebugEnabled() ? Ca prend autant de temps.

Normalement, le logger.isDebugEnabled() est un ensemble d’appels à des valeurs constantes. Et ça prend du temps parce que …​

La JVM ne croit pas que les champs final sont des constantes.

A cause de la …​ (de)serialization !!! Autrement dit, comme la JVM peut (dé)sérialiser tous les champs final ne peut pas inliner leurs appels.

java.lang.invoke

Avec cette API, on peut appeler du code dynamiquement d’une façon plus efficace que l’introspection pour plusieurs raisons :

  • Mise en cache de la sécurité
  • Pas de boxing

L’API est plus riche pour implémenter des langages. Elle est utilisée pour les lambdas, et pour la concaténation de chaînes de caractères en Java9.

Avec cet API on crée un MethodHandle qui est une espèce de pointeur de fonction. Il permet aussi de faire de l’invocation partielle. Et l’élément sur lequel il sera bindé sera considéré par le JIT comme une constante. En plus, il permet d’ajout des arguments qui ne seront pas passés à la méthode. Et de créer des MethodHandle vides ou de placer des gardes sur le MethodHandle.

Avec tout ça, Rémi recréée son Logger en utilisant exclusivement des MethodHandle. Le code est verbeux, mais on sent bien qu’il est particulièrement optimisé.

Bon, ça marche pas aussi bien que prévu. Allons donc voir le code source …​ de la JVM !

Le seul cas qui soit exploitable, c’est celui des classes anonymes (au sens de la JVM, pas du langage). Et ces classes anonymes au sens du langage sont les lambdas.

Ca marche bien.

Allons voir plus loin.

Avec une simple boucle et un logger.debug(…​) mais cette fois-ci avec log4j, on se retrouve avec des temps d’exécution différents là où il ne devraient pas être différents. Le tout à cause d’un volatile dans log4j. Ca amène Rémi à nous parler de SwitchPoint.

Et tout ce code est dans le projet Beautiful Logger. Qui lui est capable de ne pas logger sans consommer de temps CPU. En utilisant les mêmes techniques, Rémi a créé un projet de classes exotiques.

Mon avis

Je savais que ça allait dépoter, et je n’ai pas été déçu. Rémi nous en a mis plein la tronche avec des phrases qui se retrouveront sur son compte Twitter (ça n’est pas vraiment lui qui twitte).

Devoxxfr – A brief history of API

Vous pouvez retrouver les slides de la présentation sur Google Drive.

Pourquoi ?

Parce qu’un juge fédéral lui a demandé quand les API sont apparues dans l’histoire.

Quand sont apparues les API ?

Le terme subroutine a été inventé à l’époque de Von Neuman par celui-ci et un collègue. Ils expliquent dans la publication où apparaît le terme que les programmes se décomposent en opérations simples et réutilisables. Ce qui est curieux, c’est que Wilkes a reçu un prix Turing pour une idée similaire …​ mais surtout pour l’avoir implémenté. La machine utilisée était EDVAC, un énorme ordinateur de 1949. 1 million de fois plus lent que nos ordinateurs, avec 512 séries de 17 bits de mémoire …​ Et tout un tas d’autres éléments qui montrent à quel point il était rudimentaire. Et il a réussi à aller plus vite que Von Neuman en créant un ordinateur plus simple. Plus simple, mais qui a été mis à jour plusieurs fois. En codant son premier programme, Wilkes se rend compte qu’en l’état, il devrait corriger ses erreurs pour le reste de sa vie. Pour éviter ça, il demande à un de ses doctorants de travailler sur une architecture de sous-routines. Son code supportait de multiples niveaux de sous-routines, le passage de fonctions comme arguments.

Le concept a été introduit dans le premier livre sur la programmation : « The preparation of programs for an electronic digital computer« .

Et dans l’article présentant leur bibliothèque, ils expliquent aussi le besoin de debugger (qui fonctionnait sur leur ordinateur !). Il y explique également que la documentation de ces sous-routines est peut-être la tâche la plus complexe. Tout ça dans un papier de seulement deux pages !

Pourquoi Wilkes et Wheeler ne parlaient pas d’API ? C’est simple : il n’y avait qu’un ordinateur. Donc ils n’avaient pas besoin d’interagir avec d’autres ordinateurs. Par contre, quand ils ont recodé leurs fonctions dans la version améliorée d’EDVAC, ils ont donné vie à la notion d’API.

Par contre, le mot semble être apparu en 1968. Ils comprenaient bien que l’API avait un sens au-delà de son implémentation. Mais aussi que les API apparaissent dès qu’il y a une bibliothèque de code. En quelque sorte, elles en sont une propriété émergente.

Qu’est-ce qui constitue une API ?

Une API est ensemble de fonctionnalités indépendantes de leur implémentation, afin de permettre des implémentations variables.

Quelques exemples

  • L’API Fortran créée en 1954 contenait 28 fonctions mathématiques …​ et est toujours une partie de l’API standard de ce langage.
  • Kernigham & Ritchie écrivaient dans la doc du C que leur API doit être portable, afin de permettre aux programmes de s’exécuter sur de multiples plateformes sans changer le code. Comme Java, en fait.
  • Sur les terminaux VT100, une API permettait de se déplacer sur l’écran. La même qui est encore utilisée dans les émulateurs de terminaux modernes.
  • Le bios d’un PC fournit des API pour communiquer avec le hardware.
  • Dans un téléphone portable, le processeur central parle au processeur radio en utilisant l’API des modems.
  • Adobe PostScript pose la question : est-ce un langage ou une API. Autrement dit y a-t-il une différence entre un langage et une API ?
  • Sur le web, des sites comme delicious fournissent des API web qui permettent de les utiliser.

Qu’en retenir

Les API viennent en multiples formes. Elles sont éternelles. Elle peuvent créer des industries entières. Et enfin elles sont les moyens pour un composant d’un système informatique pour en utiliser un autre. Elles sont la glue qui tient le monde numérique.

Petite digression légale

On a toujours pu réimplémenter les API d’autres fournisseurs. Et chacun des exemples présentés a donné lieu a des réimplémentations. Même l’EDSAC a été réimplémenté, en 1959, au Japon !

Evidement, depuis le proccès Oracle/Google, les API sont sous la menace légale.

Si Oracle gagne, il ne sera plus possible de réimplémenter une API. Ou tout au moins, il faudra l’accord de l’auteur initial. Des produit comme GNU, Wine, Android, Samba, …​ ne pourraient pas exister.

Autrement dit, le droit de réimplémenter des API est crucial pour de multiples raisons.

Conclusion

Les API existent depuis toujours, et sans ça, l’informatique n’existerait pas. Et ce serait bien de leur permettre de continuer à exister.

Mon avis

Du début à la fin, la présentation était excellente. Vive, bien menée, claire dans son propos (qui est avant tout une histoire de l’API), c’était vraiment une bonne façon de rentrer dans le sujet.

Devoxxfr – Lazy Java

Mario vient nous parler de la progammation lazy, fortement utilisée en programmation fonctionnelle, mais rare en style impératif. Qu’est-ce que c’est ? C’est une stratégie d’évaluation qui fait en sorte que la valeur ne soit calculée que quand elle est utilisée. En programmation fonctionnelle, une fonction stricte évalue ses valeurs immédiatement, quand une fonction lazy évalue ses valeurs uniquement quand elles sont nécessaires.

Et donc, globalement, Java est un langage strict …​ A quelques exceptions près (les opérateurs booléeans, l’opérateur ternaire, certaines structures de contrôle, et les streams). Mais on peut rendre Java beaucoup plus lazy. Ca rend par exemple l’opérateur ternaire difficile à remplacer par une fonction …​ sauf en utilisant des lambdas (en l’occurence les Supplier<T>). C’est utilisé dans les apis de log modernes, par exemple (voir dans log4j 2).

Et franchement, c’est une sacrément bonne méthode d’optimisation : rien ne va plus vite que de ne pas appeler le code !

Et les streams utilisent ça : on peut créer un stream d’entiers infini : IntStream.iterate(1, i → i+1). Et du coup, un stream, ça n’est pas une structure de données, mais la spécification de données qu’on peut obtenir. Et grâce à ça, l’aspect lazy du code des streams permet la séparation des responsabilités. Et pour Mario, c’est cet aspect, plus que la compacité du code, qui rend les streams utile.

Et là, Mario nous sort un exemple de calcul de nombre premiers classique d’abord, puis récursif avec un head et un tail sur la liste des nombres. Cet exemple ne marche pas …​ Parce qu’un stream ne peut être terminé qu’une fois. Et que sa récursion n’est pas lazy, du coup, pouf, une boucle infinie ! Alors qu’en Scala, il y a une lazy concatenation (l’opérateur #::) qui aurait permis au premier exemple de marcher.

Donc, Mario recrée une liste lazy à base de head/tail et de Supplier<T>. Et je comprend à peu près comment ça marche ! L’explication graphique du code mérite vraiment d’être vue.

Et pour les afficher, on peut utiliser une itération externe ou interne. Et sur sa collection magique, on peut aussi (et surtout) utiliser la récursion. La récursion, c’est pratique. Mais en Java, ça peut conduire à des StackOverflow. Parce que Java n’implémente pas la tail call optimization, qui permet des récursions infinies en enlevant certaines méthodes de la pile d’appel. Et Scala l’implémente également, bien sûr (par contre il faut annoter les méthodes où on veut l’appliquer).

Par contre, la tail recursion ne marche que si la méthode récursive est appelée en dernier.

Heureusement, on peut utiliser la technique des trampolines pour permettre cette récursion infinie. En fait, le trampoline, c’est une manière de transformer une récursion en une suite d’appels de méthodes, à grand coups de Supplier<T>.

Et pour finir, Mario se propose de nous implémenter de l’injection de dépendances lazy. Et honnêtement, qund il fait la liste des défauts de l’injection de dépendances classique, ça fait peur.

Et pour ça, il introduit la monade Reader qui est une façon commode d’invoquer une fonction dans un contexte.

Et ça marche d’une façon très chouette, voire même lisible.

Conclusion

L’approche lazy, c’est quand même la meilleure des optimisations parce que ça permet de n’exécuter que le code vraiment utile. Par ailleurs, la démarche fonctionnelle apporte souvent des intérêts en remettant au moment de la compilation des comportements qui doivent y être traités.

Mon avis

j’avais déja vu sur Twitter les slides de Mario, je crois, ou au moins une partie, que je n’avais alors pas parfaitement compris. Avec la présentation en live, j’ai beaucoup mieux compris, et j’ai pris une claque. C’est vraiment chouette.

Devoxxfr – Les exceptions, oui, mais pas n’importe comment

Evaneos est une boîte d’une dizaine d’années, avec un code qui a le même âge. Charles a mené des campagnes de refactoring pour faire évoluer le code, et a beaucoup lutté contre les exceptions. Dans la plupart des systèmes, les exceptions sont simplement loggées. C’est une traduction de l’exception, et ça peut ne pas être bonne idée. Et ça pollue les logs. En bonus, quand les logs sont émis en critique, ils génèrent des alertes (et déclenchent potentiellement des astreintes et de la fatigue). Donc, il faut utiliser le bon niveau de log. De la même manière, attraper toutes les exceptions sans détail, ça peut avoir des effets indésirables. Donc, il vaut mieux traiter les exceptions de façon indépendante. Mais ça fait vite beaucoup de code. Et n’hésitez surtout pas à traduire les exceptions d’un contexte à l’autre, pour les rendre compréhensibles par les utilisateurs de votre code.

Mon avis

Je m’attendais à plus de cette présentation. Mais si elle a marché, c’est qu’il y a du public, et donc qu’on peut faire mieux.

Devoxxfr – Quand une documentation devient un problème (et que faire alors)

D’abord, la doc, il faut en écrire, mais il ne faut pas répondre RTFM aux utilisateurs.

Chez Criteo, ils ont donc une stack classique : Jenkins, Gerrit, Perpetuo/Rundeck, …​ Avec de la doc (environ 250 pages dans Confluence) raisonnablement bien faite : de la doc utilisateur, de la doc de référence.

Pourtant, chaque jour, il y a des utilisateurs qui posent des questions sur Slack …​ qui ont déja la réponse dans la doc. Parce que les utilisateurs ne lisent pas la doc. D’ailleurs, la FAQ non plus n’est pas lue.

Et quand les gens lisent la doc, parfois ils n’y croient pas. Ou alors ils la dupliquent. Et c’est mal. Et enfin, elle n’est jamais assez bien écrite.

Comment éviter ça ? Benoît essaye d’arrêter d’écrire de la doc. A la place, il fait en sorte que les outils soient simples à utiliser. Et pour ça, si il faut supprimer des fonctionnalités …​ il les supprime. Par ailleurs, donnez immédiatement l’information utile, sans passer par des intermédiaires. (par exemple, pas de « allez voir dans les logs de machin », mais un lien direct vers « les logs de machin »). Il met aussi en place des interfaces dédiées aux différentes catégories d’utilisateurs.

Enfin, supprimer les exceptions ! C’est-à-dire faire en sorte qu’il n’y ait pas de cas exceptionnels.

Autrement dit, la documentation utile, mais elle a un coût d’entrée et de maintenance. Si il faut expliquer un outil c’est qu’il faut le simplifier.

Mon avis

C’est bien de montrer que la doc révèle souvent un manque dans la conception de l’interface d’un logiciel. Mais il y a en revanche des cas où elle reste utile. Et dans ces cas-là, on fait quoi ?