In the last post, we showed how lists look like and discussed recursion concepts and the
member predicate. Today we will look into some further list functions: appending and reversing, and the concept of accumulators.
This post is based on this tutorial.
Another built-in predicate in Pilog is the "append" predicate, which takes three arguments: The first two are the sublists and the third one is the concatenated list.
: (? (append (a b) (c) @X)) @X=(a b c) -> NIL
Let's see how many possibilities we have to create
[a,b,c] out of two sublists:
: (? (append @X @Y (a b c))) @X=NIL @Y=(a b c) @X=(a) @Y=(b c) @X=(a b) @Y=(c) @X=(a b c) @Y=NIL -> NIL
Let's now take a look at the definition of
append (you can find it in the
pilog.l library file of your PicoLisp installation). Like
member, it is defined recursively. The base case is appending a list
@L to an empty list
NIL, which means that the result is equal to the final list
(be append (NIL @X @X))
If we are at the base case, the resulting list is equal to the second argument. Now, if the first argument only had one item
@A, the new list would be equal to the second argument (now called
@Y) prepended by the head of of the first list, right? And this can be repeated recursively, like this:
(be append ((@A . @X) @Y (@A . @Z)) (append @X @Y @Z))
So, to be precise, the (Prolog) predicate name
append was maybe not the best choice.
concatenated would have fit better (see SWI prolog docs). Let's trace what is happening:
: (? append (append (a b) (c) @X)) 2 (append (a b) (c) (a . @Z)) 2 (append (b) (c) (b . @Z)) 1 (append NIL (c) (c)) @X=(a b c) -> NIL
The first list is reduced down to an empty list, then the second argument is set equal to the third argument. After that, the third argument is "filled up" until we reach the final result.
As you can see, it works, but it takes a lot of steps and can become quite unefficient quickly.
Let's look at another predicate called
reverse (not built-in in pilog). It returns true if the elements of the first argument are in reverse order compared to the second argument:
: (? (reverse (a b c) @X)) @X = (c b a)
We could implement it using
append again with a recursive approach: starting from an empty list and then building it up by concatenating the head.
(be reverse (NIL NIL)) (be reverse ((@A . @X) @R) (reverse @X @Z) (append @Z (@A) @R))
However, due to the low efficiency of
append, this is not to be recommended. Let's find a better approach.
A better idea is to use an accumulator to solve this task. An accumulator is basically a list that "takes up" the elements that are processed.
(be accRev ((@H . @T) @A @R) (accRev @T (@H . @A) @R) ) (be accRev (NIL @A @A))
The result (which is the third argument) corresponds exactly to the reversed list. So all we need to do is then to define our
(reverse (@L @R)) predicate as follows:
(be reverse (@L @R) (accRev @L NIL @R))
Let's trace it in order to check if it does what we think it does:
: (? reverse accRev ( reverse (a b c) @X)) 1 (reverse (a b c) @R) 1 (accRev (a b c) NIL @R) 1 (accRev (b c) (a) @R) 1 (accRev (c) (b a) @R) 2 (accRev NIL (c b a) (c b a)) @X=(c b a) -> NIL
The advantage is that the result is directly available after the last step, while our "naive" reverse needed to travel the whole recursion tree back up.
Speaking of reversed lists, one example should not be missing: How to write a simple palindrome checker. Actually we have all we need already except for the actual
palindrome predicate which only takes one argument, a list:
(be palindrome (@L) (reverse @L @L) )
reverse, we can use one of our previously defined predicates. Let's test it:
: (? (palindrome ( r o t a t o r))) -> T : (? (palindrome ( a b c d e f g))) -> NIL
You can find the sources for all examples here.
In the last post of the Prolog Crash Course series, we will take a look at "Cuts and Negations".