To give an example of what I mean, here's a typical C function that concatenates two linked lists [1]:
void concatenate(struct node *a,struct node *b)
{
if (a->next == NULL)
a->next = b;
else
concatenate(a->next,b);
}
And here's the same thing in Prolog: append([],Ys,Ys).
append([H|T],Ys,[H|Zs]):-
append(T,Ys,Zs).
append/3 is a Prolog predicate that can be used to concatenate two lists. It's
a little difficult to get your head around it the first time you see it but
basically one interpretation of it is that concatenating a list Ys and the
empty list yields the list Ys; and concatenating the non-empty list [H|T] (for
"Head" and "Tail") with a list Ys yields the concatenation of the tail, T, of
[H|T], with Ys.append/3 and concatenate() share the same procedural interpretation: "to concatenate two lists walk through the first list until you find the last element in its tail ("[]" in Prolog, NULL in C) and make that element be the first element of the second list [2]".
The difference is that Prolog's declarative interpretation describes not only concatenation of two lists, but also their difference. Indeed, append/3 can be called in different instantiation modes to perform both tasks:
% Zs is the concatenation of _Xs and _Ys
?- _Xs = [a,b,c], _Ys = [d,e,f], append(_Xs,_Ys,Zs).
Zs = [a, b, c, d, e, f].
% Zs \ Xs = Ys
?- _Xs = [a,b,c], _Zs = [a,b,c,d,e,f], append(_Xs,Ys,_Zs).
Ys = [d, e, f].
% Zs \ Ys = Xs
?- _Ys = [d,e,f], _Zs = [a,b,c,d,e,f], append(Xs,_Ys,_Zs).
Xs = [a, b, c] .
% Starting a variable with an underscore, like in _Xs, _Ys, _Zs, stops the
% Swi-Prolog runtime from printing the bindings of the variable and
% cluttering the screen with values you already know ;)
In short, the definition of append/3 doesn't (just) tell the computer to
concatenate two lists to yield a third list, like the C program does. Instead,
it describes a relation between three lists that is true under certain
conditions, specified by the programmer who wrote append/3. It's this property
of describing the shape of data, rather than splitting your code into data and
operations on data, that mark code as declarative rather than procedural [3].________________
[1] Copied from: https://www.codesdope.com/blog/article/concatenating-two-lin...
[2] "Behind the scenes" both Prolog interpreter and C compiler have the same "view" of computer memory and the pointers from an element in a list to the next. However, the point here is not that the Prolog interpreter "hides" this fact. If you squint a bit- it doesn't. [H|T] is the concatenation of H and T and you can very well read it as a pointer from H to the first element of T.
[3] And note of course there are more purely declarative languages than Prolog, like Answer Set Programming. But, I know Prolog and You Can't Teach an Old Dog New Tricks (old dogs know all the tricks :P).
Personally I'd set the line as being somewhere where by definition of the system, there is a barrier set up and you are no longer allowed to "see into" the implementation because it's going to do things so crazy to your declaration that allowing you to see into it would ruin it. e.g. in SQL when you send in your query the query engine is reserving the right to rewrite it in absolutely crazy ways, and in principle, you have no right to ask about what it actually did as long as you get the "right answer". This is because I then like to segue into how that barrier is somewhat illusory because it's always going to take some concrete actions and you may have need to see into those actions. But that isn't the only place to draw the line, it's just where I find it convenient to draw the line so I can ride a pet hobbyhorse. There isn't really a bright line, even though there is clearly some real difference we are trying to talk about.