Search code examples
listprologempty-list

Understanding Prolog's empty lists


I am reading Bratko's Prolog: Programming for Artificial Intelligence. The easiest way for me to understand lists is visualising them as binary trees, which goes well. However, I am confused about the empty list []. It seems to me that it has two meanings.

  1. When part of a list or enumeration, it is seen as an actual (empty) list element (because somewhere in the tree it is part of some Head), e.g. [a, []]
  2. When it is the only item inside a Tail, it isn’t an element it literally is nothing, e.g. [a|[]]

My issue is that I do not see the logic behind 2. Why is it required for lists to have this possible ‘nothingness’ as a final tail? Simply because the trees have to be binary? Or is there another reason? (In other words, why is [] counted as an element in 1. but it isn't when it is in a Tail in 2?) Also, are there cases where the final (rightmost, deepest) final node of a tree is not ‘nothing’?


Solution

  • In other words, why is [] counted as an element in 1. but it isn't when it is in a Tail in 2?

    Those are two different things. Lists in Prolog are (degenerate) binary trees, but also very much like a singly linked list in a language that has pointers, say C.

    In C, you would have a struct with two members: the value, and a pointer to the next list element. Importantly, when the pointer to next points to a sentinel, this is the end of the list.

    In Prolog, you have a functor with arity 2: ./2 that holds the value in the first argument, and the rest of the list in the second:

    .(a, Rest)
    

    The sentinel for a list in Prolog is the special []. This is not a list, it is the empty list! Traditionally, it is an atom, or a functor with arity 0, if you wish.

    In your question:

    • [a, []] is actually .(a, .([], []))
    • [a|[]] is actually .(a, [])

    which is why:

    ?- length([a,[]], N).
    N = 2.
    

    This is now a list with two elements, the first element is a, the second element is the empty list [].

    ?- [a|[]] = [a].
    true.
    

    This is a list with a single element, a. The [] at the tail just closes the list.

    Question: what kind of list is .([], [])?

    Also, are there cases where the final (rightmost, deepest) final node of a tree is not ‘nothing’?

    Yes, you can leave a free variable there; then, you have a "hole" at the end of the list that you can fill later. Like this:

    ?- A = [a, a|Tail], % partial list with two 'a's and the Tail
       B = [b,b], % proper list
       Tail = B. % the tail of A is now B
    A = [a, a, b, b], % we appended A and B without traversing A
    Tail = B, B = [b, b].
    

    You can also make circular lists, for example, a list with infinitely many x in it would be:

    ?- Xs = [x|Xs].
    Xs = [x|Xs].
    

    Is this useful? I don't know for sure. You could for example get a list that repeats a, b, c with a length of 7 like this:

    ?- ABCs = [a,b,c|ABCs], % a list that repeats "a, b, c" forever
       length(L, 7), % a proper list of length 7
       append(L, _, ABCs). % L is the first 7 elements of ABCs
    ABCs = [a, b, c|ABCs],
    L = [a, b, c, a, b, c, a].
    

    In R at least many functions "recycle" shorter vectors, so this might be a valid use case.

    See this answer for a discussion on difference lists, which is what A and Rest from the last example are usually called.

    See this answer for implementation of a queue using difference lists.