L’explicite contre l’implicite à l’ère des intelligences
L’explicite contre l’implicite à l’ère des intelligences
Pourquoi l’implicite devient fragile quand humains et agents co-produisent du code.
Dans l’article précédent, je défendais une idée simple : le code n’est pas une prose élégante. C’est un système, qu’il faut juger à sa robustesse.
Mais cette idée en appelle une autre.
Si le code est un système, alors il ne suffit pas qu’il soit agréable à écrire, fluide à lire localement, ou élégant en apparence. Il faut encore que ses structures restent visibles. Il faut que ses frontières, ses états, ses contrats, ses responsabilités et ses effets puissent être compris, repris, critiqués, transformés.
Autrement dit : il faut que le système soit explicite.
C’est là que se joue une tension très actuelle. Depuis longtemps, une partie de l’outillage logiciel valorise l’implicite : les conventions, la magie de framework, les comportements déduits, les structures invisibles, les raccourcis qui permettent d’aller vite. Cela a souvent été utile. Cela a même parfois été très productif.
Mais à l’ère des intelligences — humaines et artificielles — l’implicite change de nature.
Le problème de l’implicite n’est pas seulement qu’il est caché. C’est qu’il n’est plus garanti d’être partagé.
L’implicite a longtemps été un accélérateur
Il ne faut pas caricaturer l’implicite.
L’implicite n’est pas mauvais par nature. Il a permis de construire des outils plus fluides, des frameworks plus rapides à prendre en main, des conventions plus productives. Il a permis d’écrire moins, de répéter moins, de déplacer une partie du travail vers le framework, le langage ou l’environnement.
C’est souvent ce qui rend une technologie agréable.
Un fichier placé au bon endroit devient automatiquement une route. Une fonction nommée d’une certaine manière est reconnue par un outil. Une directive discrète change le lieu d’exécution d’un morceau de code. Une convention de nommage évite une configuration explicite. Un hook React permet de réutiliser de la logique en se branchant sur le cycle de vie d’un composant.
Tout cela peut être pratique.
Et parfois, ce confort local donne une impression d’élégance.
Mais il faut nommer ce qui se passe réellement : une partie de la structure du système n’est plus dans le code lui-même. Elle est dans le savoir préalable qu’il faut posséder pour interpréter ce code correctement.
L’implicite fonctionne bien quand tout le monde partage le même contexte.
Il fonctionne moins bien quand ce contexte devient instable.
L’implicite devient fragile quand il n’est plus partagé
Dans une équipe humaine stable, certains implicites peuvent tenir longtemps.
Les développeurs savent comment le système fonctionne. Ils connaissent les conventions locales. Ils savent pourquoi telle couche ne doit pas importer telle autre. Ils savent que telle fonction ne doit être appelée que dans tel état. Ils savent que tel hook est fragile, que telle abstraction cache un effet de bord, que telle route dépend d’une convention de fichier.
Ce savoir n’est pas toujours écrit. Mais il circule par habitude, par revue de code, par transmission orale, par familiarité avec le projet.
À l’ère des agents, cette situation devient plus fragile.
Les humains et les agents ne produisent pas le code selon la même logique interne. Les humains mobilisent des concepts, des intentions, des modèles mentaux, une compréhension causale plus ou moins explicite. Les agents produisent une continuité plausible sous contrainte, puis sont rappelés à l’ordre par le compilateur, le type checker, les tests, le lint et les règles d’audit.
Les deux peuvent produire du code valide.
Mais ils ne le produisent pas de la même manière.
Et surtout, ils ne partagent pas nécessairement les mêmes implicites.
Un agent peut introduire une forme localement plausible, compatible avec les contraintes immédiates, mais non nommée, non stabilisée, non partagée par les humains qui reprendront le système ensuite. Un autre agent, ou une autre version du même agent, pourra inférer autre chose. Un humain pourra ne pas reconnaître l’hypothèse cachée. Le code continuera peut-être à fonctionner, mais son intelligibilité collective aura diminué.
Un implicite non partagé devient une dépendance invisible du système.
Et une dépendance invisible finit toujours par coûter cher.
Une machine probabiliste face à un système qui doit rester stable
Un LLM est une machine probabiliste.
Il produit une suite plausible. Il travaille à partir d’un contexte, de patterns appris, d’exemples, de contraintes, de retours d’outils. Il ne comprend pas le code comme un humain comprend un système qu’il a conçu, habité, corrigé, maintenu, confronté au réel.
Pourtant, le code qu’il produit doit rejoindre un artefact qui, lui, ne peut pas rester probabiliste.
Le système logiciel doit tendre vers quelque chose de stable : des états définis, des transitions contrôlées, des contrats vérifiables, des effets localisés, des erreurs observables.
Il y a donc une tension centrale : une machine probabiliste participe à la production d’un système qui doit rester prévisible, testable et contrôlable.
L’explicite ne supprime pas cette tension.
Il la réduit.
Plus un système repose sur des implicites, plus l’agent doit inférer. Plus il doit inférer, plus la dérive devient probable. À l’inverse, plus les concepts sont rendus visibles, moins l’agent doit deviner ce qui est attendu.
L’explicite n’annule pas la nature probabiliste du modèle. Il borne ce qu’il a besoin de deviner.
C’est pour cela que l’explicite n’est pas seulement une aide à la lecture. C’est le langage commun de production d’un système quand ses auteurs n’ont pas le même mode de raisonnement.
Quand les producteurs du code ne raisonnent pas de la même manière, l’explicite devient une condition de coordination.
L’explicite crée une portabilité cognitive
Il y a une autre conséquence importante.
Quand les concepts sont explicites, le langage devient en partie secondaire.
Prenons un exemple simple : les états possibles d’un système.
En TypeScript, on peut les représenter avec une union discriminée :
TypeScript
type RecorderState =
| { type: "idle" }
| { type: "recording"; sessionId: string }
| { type: "paused"; sessionId: string }
| { type: "error"; reason: string };En Rust, on écrirait plutôt :
Rust
enum RecorderState {
Idle,
Recording { session_id: String },
Paused { session_id: String },
Error { reason: String },
}En Swift :
Swift
enum RecorderState {
case idle
case recording(sessionId: String)
case paused(sessionId: String)
case error(reason: String)
}En Kotlin :
Kotlin
sealed interface RecorderState {
data object Idle : RecorderState
data class Recording(val sessionId: String) : RecorderState
data class Paused(val sessionId: String) : RecorderState
data class Error(val reason: String) : RecorderState
}Ce ne sont pas les mêmes langages.
Mais c’est la même forme conceptuelle.
On ne lit plus seulement du TypeScript, du Rust, du Swift ou du Kotlin. On lit un ensemble fini d’états possibles. On lit une intention de modélisation. On lit une structure qui dit : voici les cas reconnus par le système.
C’est ce que j’appellerais une portabilité cognitive.
L’explicite ne rend pas seulement le code lisible. Il le rend lisible indépendamment du langage.
L’implicite, lui, voyage moins bien. Il est souvent local à un framework, à une convention, à une directive, à une magie runtime, à un savoir d’équipe. Il peut être très efficace à l’intérieur de son contexte. Mais dès que l’on change de langage, de framework, d’agent, d’équipe ou d’époque, il devient plus fragile.
L’explicite rend les formes reconnaissables.
Et des formes reconnaissables facilitent la convergence.
Une grille pour analyser l’implicite
On peut alors proposer une grille simple.
Pour analyser l’implicite dans un code réel, il faut croiser trois dimensions.
La première dimension, ce sont les concepts système :
- frontières ;
- contrats ;
- validation ;
- invariants ;
- états explicites ;
- responsabilité et autorité ;
- transformations déterministes ;
- preuve ;
- observabilité.
La deuxième dimension, ce sont les strates où ces concepts peuvent apparaître :
- le langage ;
- le framework ;
- les librairies ;
- l’architecture applicative ;
- les conventions d’équipe ou de projet.
La troisième dimension, c’est leur mode d’expression : explicite ou implicite.
À chaque fois, la question est la même :
Est-ce que ce concept est rendu visible dans le code, ou est-ce qu’il repose sur une convention, une magie, une abstraction ou un savoir préalable ?
Ce n’est pas une condamnation de toute abstraction.
Une bonne abstraction cache le bruit.
Une mauvaise abstraction cache le système.
La différence est essentielle.
Quand une abstraction cache un détail sans importance, elle nous aide. Quand elle cache une frontière, un état, une responsabilité, un effet de bord ou un invariant, elle rend le système plus difficile à comprendre et plus facile à faire dériver.
Voici une version synthétique de cette grille :
| Concept | Strate | Forme explicite | Forme implicite | Risque |
|---|---|---|---|---|
| Frontières | Langage | modules, visibilité, types d’entrée/sortie, imports contrôlés | imports libres, globals, conventions non vérifiées | frontières poreuses |
| Frontières | Framework | fichiers server/, client/, handlers séparés, server-only | directive discrète, colocation qui masque client/serveur | oubli de la frontière |
| Contrats | Langage | types stricts, interfaces, unions discriminées | any, strings libres, objets informels | suppositions silencieuses |
| Validation | Framework / librairie | schémas de validation aux frontières, parsing explicite | payload accepté par convention | données douteuses dans le cœur |
| Invariants | Architecture | constructeur contrôlé, modèle métier borné, transitions interdites | objets mutables partout, règles dispersées | état incohérent possible |
| États | Langage / architecture | machine à états, statechart, unions discriminées | booléens dispersés, conditions locales | transitions illégales |
| Responsabilité / autorité | Architecture | ownership clair d’une ressource ou d’une transition | plusieurs couches peuvent muter la même chose | autorité floue |
| Transformations déterministes | Code métier | fonctions pures, immutabilité, input/output clairs | effets de bord cachés | comportement difficile à tester |
| Preuve | Langage / outils | typecheck, exhaustivité, tests, contraintes | confiance humaine, conventions | oubli non détecté |
| Observabilité | Runtime / architecture | logs contextualisés, traces, IDs de corrélation | console.error, erreur générique sans contexte | impossible de comprendre la production |
L’intérêt de cette grille n’est pas d’être exhaustive. Elle sert surtout à déplacer la question.
On ne demande plus seulement : “est-ce que ce code est propre ?”
On demande : “où les concepts importants du système sont-ils visibles ? Et où ont-ils disparu derrière de l’implicite ?”
Exemple : les frontières dans Next.js
Prenons un exemple contemporain : les Server Actions de Next.js.
Le problème n’est pas que l’outil soit mauvais. Le problème n’est pas non plus qu’il impose de brouiller les frontières. On peut parfaitement définir des actions serveur séparément, dans des fichiers dédiés, et marquer clairement la coupure.
Mais le framework rend possible, via une directive, une forme de colocation très pratique entre le code frontend et l’action serveur. Ce confort local peut donner une impression d’élégance.
Pourtant, la frontière n’a pas disparu.
Elle s’est seulement faite plus discrète.
Derrière cette simplicité apparente, il existe toujours une mécanique réelle : fonctions serveur appelables via le réseau, contrôles d’authentification et d’autorisation à refaire dans l’action elle-même, variables capturées envoyées puis renvoyées, chiffrement, dépendance au build.
Le framework en prend une partie en charge, mais l’abstraction ne supprime pas la complexité. Elle la déplace et la masque partiellement.
Dans la grille, on pourrait dire :
- concept : frontière ;
- strate : framework ;
- forme explicite : séparation claire entre client et serveur ;
- forme implicite : colocation très pratique qui rend la frontière moins visible ;
- risque : oublier les exigences propres à cette frontière.
Encore une fois, le problème n’est pas l’outil.
Le problème, c’est la facilité avec laquelle un confort local peut faire oublier une frontière réelle.
Exemple : les hooks React
Les hooks React sont un autre exemple intéressant.
Ils ont apporté quelque chose de très puissant : la réutilisation de logique.
Avant les hooks, React rendait déjà les vues composables. Avec les hooks, il devenait possible de rendre une logique locale réutilisable elle aussi : état, effets, subscriptions, accès au cycle de vie, comportements partagés.
C’était un vrai progrès.
Mais ce progrès s’est accompagné d’une forme d’implicite très forte.
Les hooks ne sont pas du JavaScript ordinaire. Ils obéissent à des règles spécifiques : ordre d’appel contraint, impossibilité de les appeler conditionnellement, préfixe use, dépendances à maintenir dans des tableaux, lint dédié pour signaler les usages invalides.
Une partie du système vit donc dans des règles implicites du framework.
On peut bien sûr apprendre ces règles. On peut les outiller. On peut les respecter. Mais il faut reconnaître ce qu’elles sont : un savoir préalable nécessaire pour interpréter correctement le code.
Et le problème devient plus net quand les hooks servent à porter un comportement complexe.
Quelques useState, quelques useEffect, quelques callbacks, quelques handlers, et soudain le composant ne rend plus seulement une vue. Il cache un petit système : états, transitions, effets, ressources, invariants, cycle de vie.
La vue devient le lieu où l’on a mélangé le comportement.
La logique est réutilisable, mais elle n’est pas forcément explicite.
Les hooks ont rendu la logique réutilisable. Ils ne l’ont pas rendue explicite.
Sortir la logique de React
Une direction possible n’est pas de supprimer les hooks.
C’est de les remettre à leur place.
Dans cette approche, React reste une couche de vue et d’adaptation. La logique importante, elle, vit dans un modèle explicite hors React.
Le modèle porte :
- les états ;
- les événements ;
- les transitions ;
- les effets ;
- les ressources ;
- les invariants ;
- les données exposées à l’interface.
React ne fait que s’abonner au modèle et lui transmettre des événements utilisateur.
On pourrait représenter l’architecture ainsi :
TXT
Modèle explicite → Snapshot / ViewModel → Vue ReactLe hook devient alors un adaptateur, pas le lieu principal de la logique.
Par exemple, pour un système d’enregistrement simplifié :
TypeScript
const recorderModel = createRecorderModel({
services: {
recordingApi,
uploader,
createMediaRecorder,
},
});
recorderModel.send({ type: "start" });
recorderModel.send({ type: "pause" });
recorderModel.send({ type: "resume" });
recorderModel.send({ type: "discard" });Le modèle expose ses états et ses événements :
TypeScript
type RecorderState =
| { type: "idle" }
| { type: "recording"; sessionId: string; uploadedSegments: number }
| { type: "paused"; sessionId: string; uploadedSegments: number }
| { type: "saving"; sessionId: string }
| { type: "saved"; recordingId: string }
| { type: "discarded" }
| { type: "error"; reason: string };
type RecorderEvent =
| { type: "start" }
| { type: "pause" }
| { type: "resume" }
| { type: "segment-ready"; blob: Blob }
| { type: "save" }
| { type: "discard" };Puis React s’abonne :
TypeScript
function useRecorderModel(model: RecorderModel) {
const snapshot = useSyncExternalStore(
model.subscribe,
model.getSnapshot,
model.getSnapshot,
);
useEffect(() => {
model.mount();
return () => model.unmount();
}, [model]);
return {
snapshot,
send: model.send,
};
}Et le composant ne porte plus le comportement principal :
TSX
function RecorderScreen({ model }: { model: RecorderModel }) {
const recorder = useRecorderModel(model);
return (
<RecorderView
snapshot={recorder.snapshot}
onStart={() => recorder.send({ type: "start" })}
onPause={() => recorder.send({ type: "pause" })}
onResume={() => recorder.send({ type: "resume" })}
onSave={() => recorder.send({ type: "save" })}
onDiscard={() => recorder.send({ type: "discard" })}
/>
);
}Le détail exact de l’API importe moins que le déplacement conceptuel.
Le hook ne cache plus une combinaison d’états, d’effets, de ressources et de règles implicites. Il relie React à un modèle explicite.
Des bibliothèques comme XState vont déjà dans cette direction : la logique est externalisée dans un modèle explicite, et React sert surtout de couche d’affichage et d’adaptation.
XState n’est pas le seul exemple. Redux allait déjà dans cette direction : sortir la logique de transition des composants, représenter les changements par des actions, et faire évoluer l’état dans des reducers testables séparément. Même useReducer, utilisé avec un reducer défini hors du composant, peut être une étape vers plus d’explicite. La différence n’est donc pas entre “hooks” et “pas hooks”. Elle est entre une logique cachée dans le composant et une logique rendue visible dans une forme autonome : reducer, store, state machine ou modèle explicite.
Le problème n’est pas qu’un hook appelle cette logique. Le problème apparaît quand le hook devient lui-même le lieu où l’état, les transitions, les effets et les responsabilités se mélangent.
La question n’est donc pas : “faut-il utiliser ou non des hooks ?”
La question est : “quand un comportement devient-il trop systémique pour rester caché dans des hooks ?”
Ce qu’il ne faut pas mal comprendre
L’explicite n’est pas une religion.
Il ne s’agit pas de tout rendre lourd, verbeux, cérémoniel. Il ne s’agit pas de refuser les conventions, les frameworks, les abstractions ou les raccourcis.
Un système entièrement explicite mais mal organisé peut être illisible. L’explicite ne suffit pas à produire la clarté.
Il faut donc éviter une mauvaise conclusion : plus de code n’est pas automatiquement plus de robustesse.
Ce qui compte, c’est de rendre explicites les concepts qui portent réellement le système.
Une convention locale peut très bien fonctionner si elle ne masque rien d’essentiel. Une abstraction peut être excellente si elle cache du bruit, pas une responsabilité. Un framework peut augmenter la robustesse s’il rend les bonnes frontières visibles au lieu de les diluer.
L’objectif n’est pas d’opposer naïvement explicite et implicite.
L’objectif est de savoir où l’implicite devient dangereux.
Il devient dangereux quand il cache une frontière, un état, un invariant, une autorité, un effet de bord, une hypothèse de sécurité, une règle de validation, une transition métier ou un comportement de production.
C’est là qu’il faut rendre visible.
C’est là qu’il faut nommer.
C’est là qu’il faut modéliser.
Conclusion — l’explicite comme support de convergence
À l’ère des intelligences, l’explicite n’est pas seulement une préférence de style.
Ce n’est pas seulement une manière d’aider les humains à lire du code.
C’est une condition de coordination entre des producteurs de code qui ne partagent pas nécessairement les mêmes implicites.
Un humain peut lire un système avec un modèle mental. Un agent peut produire une forme plausible sous contrainte. Un autre agent peut en produire une autre. Un futur mainteneur peut arriver sans connaître l’histoire du projet.
Ce qui leur permet de converger, ce n’est pas un implicite commun.
C’est une structure explicite commune.
L’explicite rend les concepts visibles. Il réduit la part de devinette. Il rend les formes plus portables d’un langage à l’autre. Il rend les frontières, les états, les responsabilités et les effets plus difficiles à oublier.
Il ne garantit pas que le système est bon.
Mais il rend possible une chose essentielle : le reprendre, le discuter, le corriger, le faire évoluer.
L’implicite peut accélérer.
L’explicite permet de transmettre.
Et dans un monde où humains et agents écrivent ensemble, ce qui ne se transmet pas finit par dériver.
Une remarque après lecture ?
Si vous souhaitez envoyer un mot au sujet de cet article, vous pouvez écrire ici. Je partage ici parce que le sujet m’intéresse et que je veux apprendre des autres. Merci pour vos retours, surtout lorsqu’ils sont formulés avec soin.