I quattro peccati capitali di Prolog: quando la programmazione logica si fa dannosa
Programmazione logica in crisi: i quattro errori fatali nello sviluppo Prolog
Prolog ha un fascino da outsider. Mentre la maggior parte degli sviluppatori si affida a linguaggi imperativi e orientati agli oggetti, chi sceglie Prolog segue una strada diversa. Si ragiona in termini di logica dichiarativa, un approccio che può sembrare elegante e potente. Finché non ti ritrovi con un programma che si comporta in modo imprevedibile.
In realtà, bastano pochi principi chiari per scrivere codice Prolog solido. Ignorarli significa rischiare risultati errati, soluzioni mancanti o codice difficile da testare. Ecco i quattro errori più comuni che affliggono chi lavora con Prolog.
Il killer silenzioso delle soluzioni
Può capitare di scrivere un predicato che funziona bene con input specifici. Poi, dopo mesi, arriva una query più generica e il predicato non restituisce le soluzioni che dovrebbe.
La causa spesso è l’uso eccessivo di costrutti “impuri” come l’operatore cut (!/0), il costrutto if-then-else ((->)/2) e predicati come var/1. Questi strumenti rendono il codice più semplice da scrivere, ma ne limitano la generalità. Funzionano bene se si pensa in modo procedurale, ma tradiscono la natura dichiarativa di Prolog.
% Errore comune: usare il cut per “ottimizzare”
factorial(0, 1) :- !.
factorial(N, F) :-
N > 0,
N1 is N - 1,
factorial(N1, F1),
F is N * F1.
% Query generica: ?- factorial(N, F).
% Risultato: N = 0, F = 1
% Poi fallisce — perde infinite soluzioni valide!
Al posto di questi strumenti, conviene usare strutture dati pulite, predicati di constraint come dif/2 e meta-predicati di ordine superiore. In questo modo il codice diventa più generale e facilmente testabile.
Il rischio di modificare il database
Molti sviluppatori, appena scoprono assertz/1 e retract/1, ne fanno un uso eccessivo. L’idea di modificare il database durante l’esecuzione sembra potente e flessibile. Ma si tratta in realtà di una tecnica che introduce dipendenze nascoste e rende il codice fragile.
Quando si alterano dati globali con assert e retract, si creano contratti invisibili. Se un predicato viene chiamato in un ordine diverso da quello previsto, il risultato può fallire senza alcun motivo apparente. Lo stato non è visibile nel codice: si nasconde nel database globale. Anche il testing diventa problematico, perché ogni test può lasciare residui che stört la prossima esecuzione.
Altra soluzione: passare lo stato esplicitamente attraverso gli argomenti dei predicati. In questo modo la logica resta chiara e visibile.
Il problema della stampa
Un errore che colpisce anche molti sviluppatori esperti è il seguente:
% Anti-pattern: mescolare logica e side effect
solve_and_print :-
solution(S),
format("The solution is: ~q~n", [S]).
Questo codice è difficile da testare. Non si può trattare l’output come un dato. E il predicato non può essere usato come vera e vera relazione. Il risultato esiste solo sul terminale,而不是 nel programma.
Il fix è semplice: separare le funzioni. Il predicato deve descrivere la soluzione in termini di logica pura. Poi, al livello superiore, si gestisce la presentazione. Se serve una formattazione speciale, si può usare la notazione DCG. così lässt sich il codice testare, riutilizzare e ragionare in modo formale.
Il problema dei costrutti obsoleti
Prolog ha fatto progressi. Da vent’anni esiste Constraint Logic Programming over Finite Domains (CLP(FD)). Molti sistemi moderni offrono strumenti di alto livello che rendono il codice più chiaro e più potente.
Tuttavia molti sviluppatori continuano ad usare (is)/2, (=:=)/2 e (>)/2 perché “sono sempre funzionati”. Il costo è real: il codice diventa più difficile da insegnare, da leggere e da понять. Questi costrutti di basso livello confondono la semantica dichiarativa con la meccanica operativa.
Usando invece i constraints, la semantica dichiarativa diventa trasparente. Il codice appare come una specificazione, non come una procedura.
Il caso del factorial “horror”
Ecco un esempio che combina tutti questi errori:
% La versione problematica
horror_factorial(0, 1) :- !.
horror_factorial(N, F) :-
N > 0,
N1 is N - 1,
horror_factorial(N1, F1),
F is N * F1.
% Query: ?- horror_factorial(N, F).
% Risultato: N = 0, F = 1 [e poi si stoppa]
Questo codice viola tre principi:
- Il cut rompe la possibilità di backtracking verso soluzioni alternative
- L’aritmetica di basso livello (
is,>) richiede che entrambi gli argomenti siano già instantiati - Il codice non è generalizzabile: funziona come una funzione, but non come una relazione
Se si vuole queryare “tutte le N e F dove factorial(N, F) vale”, si ottiene un risultato di errore.
Come scrivere codice Prolog migliore
Il percorso verso un Prolog robusto è semplice:
- Preferisci pattern dichiarativi rispetto a trucchi imperativi. Use constraints e meta-predicati al posto di cuts e type checks.
- Passa lo stato esplicitamente. Thread it through arguments o use context notation.
- Separare logica da presentazione. Descrivi le soluzioni puramente; al top-level si ha la display.
- Usa costrutti moderni. CLP(FD) e altri strumenti di alto livello esistono perché sono meglio.
Scrivere Prolog code che risponde a query generali, che si legge come una specifica e che segue la natura dichiarativa del linguaggio. Allora Prolog mostra la sua vera potenza. E il mantenimento diventa un piacere,而不是 un nightmare.