PrologEZ
Data & computation · Lesson 18 of 43

Lists from scratch

Lists as cons/nil terms, the built-in [H|T] syntax, and member, find, position, concat and count.

Lists in Prolog

Lists are defined via two constructors:

  • nil, the empty list, containing no elements;
  • cons, the constructor taking an element H and a list T, and generating the list cons(H, T).

For instance cons(a, cons(b, cons(c, nil))) represents the list a, b, c. Lists are typical recursive data structures, used to represent sequences of any sort.

Prolog lists

In Prolog, lists are defined via two analogous constructors:

  • [] represents the empty list, containing no elements (a constant);
  • . stands for cons: it takes an element H and a list T, and generates the list .(H, T) (a functor of arity 2).

The sequence notation simplifies writing lists: .(H, T) can be written as [H|T], and .(H, .(H', T')) as [H, H'|T']. There, the empty list can be omitted. For example [a, b, c] represents the list a, b, c in Prolog, where a is the head of the list and [b, c] is the tail:

mgu([a, b, c], [H|T]) = {H/a, T/[b, c]}

?- [a, b, c] = [H|T]
?- [a, b, c] = [H, H2|T]
?- append('.'(a, '.'(b, [])), '.'(c, []), '.'(a, '.'(b, '.'(c, []))))
?- append('[|]'(a, '[|]'(b, [])), '[|]'(c, []), L)

Built-in list syntax

Libraries have list functions that assume you actually use these functors, and the ad-hoc syntax avoids the verbose right-associative construction: write [H1, H2, ..., Hn|T] instead of '.'(H1, '.'(H2, ..., '.'(Hn, T)...)), and [H1, H2, ..., Hn] instead of [H1, H2, ..., Hn|[]]. The forms [H|T], [], [E1, E2], [_|T] and [E1, _|_] are special cases. This is standard syntax we should always use!

Other list predicates exist in the Prolog library: member(Element, List) (similar to our find/element), reverse(List, ReversedList), and many others. Since append/3 is a relation, it also tells you about the shape of its result (file list-syntax.pl):

?- append([a, b], [c], L)
?- append([a, b], [c], [H | T])
?- append([a, b], [c], [_ | _])
?- append([a, b], [c], [_, _, _])
?- append([a, b], [c], [_, _, _, _])
?- append([a, b], [c], [_, _, E])
?- append([a, b], [c], [_, _ | T])
?- append([a, b], [c], [_, _, _ | T])

Computing with lists: recursion

Being recursive data structures, lists are typically handled by recursive rules, which incidentally is also the only way to handle repeated operations over sequences in Prolog, where there is nothing like a cycle programming construct.

The recursion scheme: since Prolog's search strategy is depth-first, in particular with clauses used orderly, top-down, termination is handled with a fact, typically coming before the recursive rule, as already seen for nat/1 and sum/3.

Typical example: member

File lists-member.pl. member/2 checks whether the first argument is a term that is a member of the list in the second argument:

member(X, [X|_]).
member(X, [_|T]) :- member(X, T).
?- member(b, [a, b, c])
?- member(b, [a, b, b])
?- member(X, [a, b, c])
?- member(blue(X), [red(a), blue(b), red(c), blue(d)])
?- member(z, X)

Remarks:

  • the search strategy is left to right through the list;
  • it finds out all the members of the list (member(X, [a, b, c]));
  • conditional membership: given a certain computed substitution (blue(X) only selects the blue elements);
  • generation of lists: member(z, X) enumerates every list that has z in it, with the position of z growing one by one.

Lists with cons and nil

File element.pl. Now the same ideas with the cons/nil construction: functors cons/2 (with head and tail as arguments) and nil/0 for the empty list, e.g. cons(a, nil) and cons(a, cons(b, nil)). Note that they are trees. Functions and predicates are generally implemented by matching, through different clauses: recursive functions have their base cases as facts, followed by the recursive rules.

% relates an element E with a list that contains it
element(E, cons(E, _)).
element(E, cons(_, T)) :- element(E, T).

How to read the specification in the relational interpretation: E is found in a list with head E; E is found in a list with tail T provided E is found in T.

?- element(b, cons(a, cons(b, cons(c, nil))))
?- element(a, cons(a, cons(b, cons(c, nil))))
?- element(40, cons(a, cons(b, cons(c, nil))))

The resolution tree of element(b, cons(a, cons(b, cons(c, nil)))):

element(b, cons(a, cons(b, cons(c, nil))))
└── element(b, cons(b, cons(c, nil)))
    ├── Yes
    └── element(b, cons(c, nil))
        └── element(b, nil)
            └── No

The first branch gives the solution; the exploration then continues (backtracking) and ends in a failure at nil.

Programming find, position, concat and count

File lists.pl:

% relates a list with one of its elements
find(cons(E, _), E).
find(cons(_, T), E) :- find(T, E).

% relates a list with one of its elements
% and its Peano position
position(cons(E, _), zero, E).
position(cons(_, T), s(N), E) :- position(T, N, E).

% relates two lists with their concatenation
% (similar to append)
concat(nil, L, L).
concat(cons(H, T), L, cons(H, M)) :- concat(T, L, M).

% relates a list and an element with occurrences
count(nil, _, zero).
count(cons(E, L), E, s(N)) :- count(L, E, N).
count(cons(E, L), E2, N) :- E \= E2, count(L, E2, N).
?- find(cons(a, cons(b, cons(c, nil))), b)
?- find(cons(a, cons(b, cons(c, nil))), d)
?- position(cons(a, cons(b, cons(c, nil))), zero, a)
?- position(cons(a, cons(b, cons(c, nil))), s(zero), b)
?- position(cons(a, cons(b, cons(b, nil))), P, b)
?- concat(cons(a, cons(b, cons(c, nil))), cons(d, nil), L)
?- count(cons(a, cons(a, cons(b, cons(a, nil)))), a, N)

Expressiveness so far. The language is now conceptually complete: we can implement a variety of algorithms to search, clone and modify algebraic-like data structures. The next features to add: more convenient ways to work with numbers (primitive values) and with lists, library functions to manipulate terms, and library functions to tweak resolution.

The same predicates over [H|T] lists

File find-position.pl. With the standard syntax, find, position and join read much better. (join is similar to append.)

% relates a list with one of its elements
find([E|_], E).
find([_|T], E) :- find(T, E).

% relates a list with one of its elements
% and its Peano position
position([E|_], zero, E).
position([_|T], s(N), E) :- position(T, N, E).

% relates two lists with their concatenation
% (similar to append)
join([], L, L).
join([H|T], L, [H|M]) :- join(T, L, M).
?- find([a, b, c], b)
?- find([a, b, c], 40)
?- position([a, b, c], zero, a)
?- position([a, b, c], s(zero), b)
?- position([a, b, b], P, b)
?- join([a, b], [c], L)

Exercise: The last element

Write last_of(List, X): X is the last element of a non-empty list written with [H|T]. For example last_of([a, b, c], X) gives X = c. There must be exactly one answer.