Homoiconic Prolog: Explain yourself!
Explaining itself to end-users is a key feature of Expert Systems; in some applications, it’s absolutely vital. It also sets them apart from the more popular machine-learning methods.
Homoiconicity
The reasoning in Prolog is done through rules with sub-goals, which are essentially queries against a knowledge base. Prolog is homoiconic, meaning we can manipulate these rules (i.e., the language itself) as data in Prolog. Turning these rules into something we can work with is known as reification. That’s the jargon sorted!
You’ll find Prolog has built-in clauses for inspecting code. We’re most interested in clause/2
, which unifies with the head and body of a predicate. A quirk of Tau Prolog, which we’re using in your browser, means this only works with “public” predicates, so we need to declare the predicate as dynamic for the demo to work. Chances are, in your dialect, you won’t need to do this.
?-
?-
That’s some rather ugly reading, but our contrived example covers a good deal of syntax, so you shouldn’t feel too lost when expanding on these ideas to make your own explainer.
But being homoiconic has two parts: not only do we need to get at the language as data, but we get to manipulate it as such. For this job, we’ll use a DCG.
DCG
We’re not going to do a deep dive on DCGs in this post. If you’re in need of a complete tutorial, you might want to start with the Chapter on DCGs in Learn Prolog Now!.
To manipulate the rule as data into an explanation, we need to choose what each of the operators means in natural language. We can then pattern-match on the operators for the DCG rules. We can also call the body to ensure any variables that can be ground are ground.
A couple of things to watch out for:
- We’ve only got predicates with up to two arguments; you’ll need to decide how you’ll handle those with more.
- Our phrase isn’t grammatically correct—we get lots of “and”s. Should this bother you, it’s a fun exercise to recurse through the generated list to replace all but the last “and” with a comma.
- We’re enforcing that
phrase/2
is called only once, for the demo, as we only need the first solution and want to aid readability. But this means the order of clauses in our DCG is important; you’ll need to add additional guards for robustness.
Also, most Prologs provide phrase/2
or phrase/3
predicates that should be used. Tau Prolog doesn’t (yet!), so we’ve written a proxy—chances are you won’t need this.
?-
Conclusion
So there we go! Our Prolog can explain itself! Well, to a degree anyway—you’ll
notice that most Expert Systems implement their own domain-specific query
language that they use to generate explanations and query their data.
Currently, our program fails to capture disjunctive clauses that aren’t
separated by ;
. These query languages can limit the expressiveness of the
query to what you can explain via your DCG or, in some cases, provide more
operators such as implication, equivalence, and universal and existential
quantifiers. Should you wish to implement such a thing, the DCG here should
provide useful inspiration for generating your explanations.
Some handy exercises to expand on this further:
- Make it grammatically correct without repeated “and”s.
- When should you end the sentence and start another?
- If there’s disjunction, can you only explain the succeeding sub-goals?
- Can you flip it to explain why a goal doesn’t succeed, by only the sub-goals that fail?
Until next time, Happy Prologing!
Paul
2025 Update
So LLMs have come along since I wrote this. As an experiment, I fed an LLM the source code and asked it to explain why foo(5)
is true. This is its answer:
The query foo(5) evaluates to true because of the following conditions satisfied by the predicates defined in the clause:
bar(Y)
- bar(2) is defined, so Y is instantiated to 2.
baz(Z)
- baz(3) is defined, so Z is instantiated to 3.
X is Y + Z
- With Y as 2 and Z as 3, the sum Y + Z equals 5, so X is instantiated to 5.
\+ (X = 2 ; X = 3)
- This condition checks that X is not equal to 2 and not equal to 3. Since X is 5, this condition is true.
something(_)
- something(_) is a placeholder indicating that this predicate succeeds for any argument, fulfilling the final condition in the rule.
Since all the conditions in the rule for foo(X) are satisfied with X as 5, the query foo(5) is true.
And then I asked it to do a rewrite of our explanation. It seems to have been thrown off by the something, something
bit, but I do think this is better for a non-technical expert system user who wants an explanation:
X = 5. This is because the sum of bar 2 and baz 3 equals 5 (2 + 3 = 5), and 5 is neither equal to 2 nor 3. Additional details are implied in the context.
So these days, it seems we can use Prolog’s reflection in conjunction with large language models (LLMs) to generate rather good explanations for expert systems. This is a tool I wish I’d had back when I was working on such a system! With a bit of prompt engineering, we’re there!