Xalg / Fix / Bonnes pratiques
Navigation
Apprendre
Recettes
Référence
Comprendre
Bonnes pratiques
En pratique
Explications

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 :

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 :

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.