Logic Programming

With logic programming, we take some axioms and compute deductions on them in order to prove some theorem

Prolog

Prolog is an imperative language running in the context of a database with some predefined clauses

These clauses are composed of terms

We can then recreate our logic stuff in this new language as such

term → atom | number | variable | struct
terms → term | term , terms
struct → atom ( terms )
fact → term .
rule → term :- terms .
query → ?- terms .

These rules are implicitly universally quantified so you don’t need to add $\forall$ each time

Queries are also implicitly existentially qualified

To start a program, we can import some predefined clauses by setting a working directory and consulting a file

From here, we can work with the file in the interpreter

only one solution

Lists

Much like Scheme, Prolog has its own way of implementing lists through a head and a tail

Here is the entire content in one markdown code block:

?- [H|T] = [a, b, c].
H = a,
T = [b, c].

?- [H|T] = [[], c | [[a], b, [], b]].
H = [],
T = [c, [a], b, [], b].

?- [H|[X|T]] = [[], c | [[a], b, [], b]].
H = [],
X = c,
T = [[a], b, [], b].

?- [H1, H2|[X|T]] = [[], c | [[a], b, [], b]].
H1 = [],
H2 = c,
X = [a],
T = [b, [], b].

Doing things with the list is also pretty simple

member(X, [X|_]).
member(X, [_|T]) :- member(X, T).

Unification

One concept that we should understand is unification

path(L, L).
path(L, M) :- link(L, X), path(X, M).

?- path(fortran, cplusplus).

This essentially defines our variables

From here, we can define equality

Arithmetic

We have our own ways of doing arithmetic as well

Substitution

Control Order

Prolog satisfies queries through some order, using goal order (leftmost subgoal) followed by rule order (first applicable rule)

start with a query as the current goal
while (the current goal is nonempty) do
  choose the leftmost subgoal
  if (a rule applies to this subgoal) then
    select the first applicable rule not already used
    form a new current goal
  else
    if (at the root) then
      false
    else
      backtrack
true

With this, we can create a search tree to investigate what our program is doing

2023-04-24_17-50.png

rainy(seattle).
rainy(rochester).
cold(rochester).
snowy(X) :- rainy(X), cold(X).

?- snowy(C).
C = rochester.

2023-04-24_17-50_2.png

We can also add some details to our control algorithm

start with a query as the current goal: G₁, G₂, ..., Gₖ (k ≥ 0)
while (k > 0) do     // the current goal is nonempty
  choose the leftmost subgoal G₁
  if (a rule applies to G₁) then
    select first applicable rule (not tried): A :- B₁,..., Bⱼ (j ≥ 0)
    let σ be the most general unifier of G₁ and A
    the current goal becomes: B₁σ,..., Bⱼσ, G₂σ, ..., Gₖσ
  else
    if (at the root) then
      false   // tried all possibilities
    else
      backtrack   // try something else
true   // all goals have been satisfied
append([], Y, Y).
append([H|X], Y, [H|Z]) :- append(X, Y, Z).

prefix(P, L) :- append(P, _, L).
suffix(S, L) :- append(_, S, L).

?- suffix([a], L), prefix(L, [a, b, c]).
L = [a]   // that's the obvious solution

L = [a];   // if we ask for more solutions
           // we get an infinite computation
           // eventually aborting (out of stack)
?- suffix([a], L), prefix(L, [a, b, c]).
L = [a];   // infinite computation

Goal Order

If we change the goal order around, we can avoid these infinite computation situations

Rule Order

Rule order can also change solutions

Cuts

If we want to improve efficiency through banning bcaktracking, we can use a cut, which is a zero-argument predicate

?- member(a, [a,b,c,a,d,a]).
true;
true;
true;
false.

?- member1(a, [a,b,c,a,d,a]).
true.

We can also use this as a form of negation

?- X=2, not(X=1).
X = 2.

?- not(X=1), X=2.
false.