Bonnes pratiques Fix
Des règles à suivre pour éviter des pièges connus — pas « comment je fais X » (ça, c'est Recettes), mais « comment j'écris cette règle pour qu'elle marche partout, pas seulement dans le cas que je viens de tester ».
Donne toujours un générateur à une variable de tête, jamais un comparateur seul
Le piège
service(svc).
calcul(Sv, S, accepte) :- service(Sv), S < 3.
Cette règle compile, et répond correctement à une requête directe :
?- calcul(svc, 2, E).
E = accepte
Mais si rien d'autre ne dérive calcul/3 — par exemple si un outil (comme fix --test, voir plus bas) calcule toutes les conséquences des règles d'un coup, sans requête pour piloter le calcul — cette règle ne produit aucun fait. Silencieusement.
Pourquoi
Il y a deux façons de faire tourner une règle :
- Dirigée par une requête (le REPL, MCP) : tu fournis une valeur pour
S(2), et le moteur n'a plus qu'à vérifier2 < 3. Un comparateur (<,>=,=) sait tester une valeur déjà là — il ne sait pas en produire une. - Par saturation (calcul de toutes les conséquences, sans requête) : pour dériver
calcul(svc, S, accepte), le moteur devrait énumérer toutes les valeurs deSqui satisfontS < 3— mais rien dans la règle ne lui en fournit. La variableS, dans la tête, n'est liée par aucun littéral relationnel du corps, seulement par un test. Sous saturation, la relation reste vide.
C'est le même principe que le calcul d'une fermeture transitive (Recettes) où chaque variable de tête est bien liée par un fait ou un appel récursif — jamais par un comparateur tout seul.
La règle à suivre
Toute variable qui apparaît dans la tête d'une règle doit être liée, dans le corps, par au moins un littéral relationnel (un fait, ou un prédicat qui finit par toucher des faits) — avant d'être passée à un comparateur ou à un calcul arithmétique.
Deux étages de protection : une variable de tête sans aucun lieur dans le corps est refusée net à la compilation (règle non sûre) — tu ne peux pas te tromper. Le cas subtil est celui de cette section : une variable liée par un comparateur seul compile (le test la lie), mais la relation reste vide sous saturation.
service(svc).
% ✗ S n'est lié que par le comparateur — vide sous saturation
calcul(Sv, S, accepte) :- service(Sv), S < 3.
% ✓ S est d'abord lié par un fait, PUIS testé — marche partout
petit_entier(0). petit_entier(1). petit_entier(2). petit_entier(3).
calcul(Sv, S, accepte) :- service(Sv), petit_entier(S), S < 3.
Le comportement en requête directe est identique — ajouter le générateur ne change rien pour qui interroge calcul(svc, 2, E), puisque 2 est déjà dans le domaine. Le générateur ne fait que rendre la règle utilisable aussi sans requête.
Dérive le générateur plutôt que de le dupliquer
Si une borne existe déjà ailleurs dans ton programme (une capacité, un compte maximal...), dérive le domaine à partir d'elle plutôt que de le recopier à la main — une seule source de vérité à maintenir :
succ(0,1). succ(1,2). succ(2,3). % la capacité vit ICI
petit_entier(0).
petit_entier(M) :- succ(N, M). % dérivé, pas dupliqué
petit_entier(M) :- succ(N, M). reste sûr : M est lié par succ(N,M), un littéral relationnel sur des FAITS, pas par un comparateur.
La limite de la technique
Cette réparation ne marche que pour un domaine fini et petit :
- Domaine fini mais grand (des milliers de valeurs) : un générateur récursif borné fonctionne, mais coûte une itération par valeur.
- Domaine non borné (« pour tout entier ») : impossible — aucune saturation exhaustive ne peut énumérer un ensemble infini. Dans ce cas, la règle ne peut être exploitée que par une requête dirigée ; un outil qui exige une saturation complète (sans requête) restera aveugle à cette relation, quoi que tu fasses côté programme.
Sur le versant compilation du même sujet — pourquoi une règle qui fabrique des valeurs est refusée, et comment la réécrire en forme décroissante : La terminaison.
Où ça mordait : fix --test (corrigé)
Historiquement, fix --test ne consultait que la factbase saturée : un test dont le corps dépendait, même indirectement, d'une règle au motif « variable de tête liée seulement par un comparateur » échouait toujours à ce runner, alors que la même requête répondait correctement au REPL ou via MCP. C'était un défaut du runner, corrigé depuis : quand le fait est absent de la saturation, le runner replie désormais sur une requête dirigée — les tests de ce motif passent, y compris en présence de négation ailleurs dans le programme (l'exception qui subsistait a été corrigée à son tour, BUG-040).
La bonne pratique du générateur reste entière pour autant : sous saturation pure, la relation demeure vide — tout ce qui lit la factbase saturée sans requête dirigée (une autre règle qui joint dessus, un agrégat sur la relation, :listing) ne verra rien. Le générateur rend la relation réelle, pas seulement interrogeable.
→ Tu cherches une recette ponctuelle ? Recettes. Pour le pourquoi du déterminisme et des types : Comprendre.