Apprendre Fix — 1. Les bases
Première étape du parcours : tu vas écrire un programme Fix, l'interroger, découvrir qu'il est typé, le faire évoluer en direct, le tester, puis calculer avec — et rencontrer deux refus, un par garantie de Fix : le typage, la terminaison. Tout se fait au clavier ; chaque sortie montrée est celle que tu verras. Compte 30 minutes.
Le fil rouge du parcours : un plan de services — qui dépend de qui, qui est lent, qui est impacté quand quelque chose tombe. Il grandira à chaque étape, jusqu'à devenir, en fin de parcours, un modèle dont on dérive du SQL et une API.
0. Installer
Depuis la racine du dépôt, ./install.sh installe le binaire fix dans ~/.local/bin (vérifie avec fix --version). Le détail est dans En pratique.
1. Décrire, puis demander
Crée un fichier services.fix :
% qui dépend de qui
depend(web, api).
depend(api, db).
depend(api, auth).
% impacté = dépend, directement ou en chaîne
impacte(S, T) :- depend(S, T).
impacte(S, T) :- impacte(S, U), impacte(U, T).
Trois faits (des vérités posées) et deux règles (des vérités conditionnelles). Note ce que tu n'as pas écrit : aucune boucle, aucun parcours de graphe. Tu as dit ce qu'est « être impacté » — une dépendance, ou une chaîne de deux impacts.
Lance :
$ fix services.fix
Fix v3.1.0 — 3 faits, 2 règles → fixpoint : 8 faits
?-
Fix a chargé ton fichier et saturé : il a appliqué tes règles jusqu'à ce que plus rien de nouveau n'apparaisse — le point fixe. Toutes les réponses sont déjà calculées avant ta première question : tes 3 faits, plus les 5 impacte que les règles en dérivent.
L'invite ?- attend tes questions. Demande : si db tombe, qui est touché ?
?- impacte(X, db).
X = web
X = api
web est touché en chaîne (web → api → db) : la deuxième règle a fait le travail. Une variable (majuscule) dans la question, et Fix énumère toutes les solutions — d'un coup, dans un ordre qui ne dépend de rien d'autre que tes faits.
C'est le renversement à retenir : tu ne diriges pas une recherche, tu interroges un ensemble déjà saturé. Même programme, même question, même réponse — toujours.
2. Et c'est typé — tout de suite
Fix n'est pas qu'un moteur de faits : il est fortement typé, et il te protège avant même de tourner. Vois par toi-même. Tes services ont un état — et un état, ça se ferme. Dans un fichier etats.fix :
type statut = up | degrade | down.
sig(etat, [service, statut]).
etat(db, up).
etat(api, degrade).
Deux nouveautés. type statut = up | degrade | down nomme un type et ferme ses valeurs : un statut est up, degrade ou down, rien d'autre. sig(etat, [service, statut]) donne la forme de etat : un service (son nom, qu'on ne ferme pas), puis un statut.
Maintenant glisse un intrus :
etat(web, casse).fix: fait mal typé : `etat(web, casse)`
position 1 : `casse` n'est pas une valeur du type `statut`.
→ :help types
casse n'est pas un statut, et Fix te le dit à la compilation — pas à l'exécution, pas dans un log trois heures plus tard. C'est l'autre garantie de Fix, à côté de la terminaison : ce qui n'a pas de sens ne compile pas. Voilà ton premier refus, celui du typage ; tu en verras un second, celui de la terminaison, plus bas. L'étape 2 fait de ce garde-fou tout un métier.
3. Faire évoluer en direct
Un nouveau service arrive. Sans quitter, ajoute un fait :
?- :assert depend(batch, db).
ok — fait asséré. Fix v3.1.0 — 4 faits, 2 règles → fixpoint : 10 faits
Fix a recalculé le point fixe. Repose la question :
?- impacte(X, db).
X = web
X = api
X = batch
Regarde ton programme courant :
?- :listing
% --- faits (4) ---
depend(web, api).
depend(api, db).
depend(api, auth).
depend(batch, db).
% --- règles (2) ---
impacte(_0, _1) :- depend(_0, _1).
impacte(_0, _2) :- impacte(_0, _1), impacte(_1, _2).
(Les variables s'affichent sous leur forme interne _0, _1 — mêmes règles, noms canoniques.) Les autres commandes : :retract Fait. retire un fait, :reload recharge le fichier (ton :assert est une expérience de session, pas une modification du fichier), :help liste tout, quit sort.
4. Tester — dès maintenant
Les tests s'écrivent dans le programme, comme le reste. Ajoute à services.fix :
test(web_touche_si_db_tombe, holds) :- impacte(web, db).
test(db_ne_depend_de_rien, fails) :- impacte(db, X).
holds : le fait doit être dérivé. fails : il ne doit pas l'être. Lance le runner :
$ fix --test services.fix
suite (principal)
✓ db_ne_depend_de_rien
✓ web_touche_si_db_tombe
─────────────────────────────────
1 suite(s) · 2 passé(s) · 0 échoué(s)
Code de sortie 0 : utilisable en CI tel quel. Prends l'habitude maintenant — chaque étape du parcours ajoutera ses tests.
5. Calculer
Ajoutons des mesures. Dans services.fix :
% latence observée, en millisecondes
latence(db, 20).
latence(auth, 10).
latence(api, 50).
% un service est lent au-dessus de 30 ms
lent(S) :- latence(S, L), L > 30.
% latence de bout en bout : la mienne + celle du service appelé
latence_chaine(S, T) :- depend(S, D), latence(S, L), latence(D, M), add(L, M, T).
Deux nouveautés : la garde L > 30 (un test qui filtre), et add(L, M, T) — le calcul s'écrit relationnellement : « L + M = T », pas une affectation.
?- lent(X).
X = api
?- latence_chaine(api, T).
T = 60
T = 70
60 par auth, 70 par db : une réponse par chaîne. Remarque le motif de la règle : L et M sont d'abord liées par des faits (latence(...)), puis seulement testées et combinées. Une variable de tête liée seulement par une garde donnerait une règle silencieusement vide sous saturation — c'est le piège n°1, détaillé dans Bonnes pratiques.
6. Le second refus : la terminaison
Après le typage, l'autre garantie. On veut compter des tentatives de reconnexion : 0, 1, 2, 3. Essaie la version « naturelle » :
essai(0).
essai(M) :- essai(N), N < 3, add(N, 1, M).$ fix services.fix
fix: terminaison non prouvable : règle croissante essai (position 0)
→ :help termination
Refusé à la compilation. La règle fabrique des valeurs nouvelles (add produit un M qui n'existait pas), et le vérificateur de terminaison ignore les gardes — il ne sait pas prouver que N < 3 borne la croissance, alors il refuse. C'est volontaire, et c'est le contrat de Fix : ce qui compile termine, garanti ; le prix est qu'il refuse parfois trop.
La réécriture qui passe : descendre au lieu de monter — pars de la borne.
essai(3).
essai(M) :- essai(N), N > 0, sub(N, 1, M).?- essai(X).
X = 0
X = 1
X = 2
X = 3
Mêmes quatre faits, zéro surveillance à faire : la descente s'arrête toute seule à 0. Le réflexe complet (et le pourquoi du refus) : La terminaison.
Ce que tu sais faire
Décrire des faits et des règles, interroger, fermer un type et voir un fait mal typé refusé, faire évoluer une session (:assert, :listing, :reload), tester (--test), calculer avec gardes et builtins, et réécrire une règle croissante refusée. Tu as croisé les deux garanties de Fix — typage et terminaison — chacune par son refus. Ton services.fix tourne et ses tests sont verts.
→ 2. Les types : approfondir le typage — le kind que tu ne déclares jamais, les ADT, les types qui se programment et ceux qui calculent.