I come up against this all the time, and I\'m never sure which way to attack it. Below are two methods for processing some season facts.
What I\'m trying to work out
method one seems wasteful since the facts are available, why bother building a list of them (especially a large list). This must have memory implications too if the list is large enough ?
Yes, method 1 takes Θ(n) memory. It's primary benefit is that it is declarative, i.e. it has a straightforward logical meaning.
Method 2, the "failure-driven loop" as Prolog programmers call it, takes constant memory, is procedural and may be preferred when you're doing procedural (extra-logical) things anyway; i.e., in I/O code it's ok to use it.
Note that SWI-Prolog has a third way of writing this loop:
forall(season(S), showseason(S)).
This only works if showseason
succeeds for each binding of season(S)
.
Lets look at your example. It is very simple, so we will imagine it being more complex. However, it seems you take for granted that side effects are essential. Let me question that a bit:
In your example you have made a very interesting discovery: The names of all seasons are of same length. What an earth-shattering insight! But wait, is it really true? The most straight-forward way to verify this, is:
?- season(S), atom_length(S,L). S = spring, L = 6 ; S = summer, L = 6 ; S = autumn, L = 6 ; S = winter, L = 6.
No need for findall/3
, no need for write/1
.
For a larger number of answers, visual inspection is not practical. Imagine 400 seasons. But we can verify this with:
?- season(S), atom_length(S,L), dif(L,6). false.
So we now know for sure that there is no season of a different length.
That is my very first answer to your question:
As long as you can, use the toplevel shell and not your own side effecting procedures! Stretch things a little bit further to avoid side-effects altogether. This is the best way to avoid failure driven loops right from the beginning.
There are more reasons why sticking to the toplevel shell is a good idea:
If your programs can be easily queried on the toplevel, it will be trivial to add test cases for them.
The toplevel shell is used by many other users and therefore is very well tested. Your own writing is often flawed and untested. Think of constraints. Think of writing floats. Will you use write/1
for floats too? What is the right way to write floats such that they can be read back accurately? There is a way to do this in iso-prolog. Here is the answer:
In ISO,
writeq/1,2
,write_canonical/1,2
,write_term/2,3
with optionquoted(true)
guarantee that floats can be read back accurately. That is, they are the same w.r.t.(==)/2
The toplevel shell shows you valid Prolog text. In fact, the answer itself is a query! It can be pasted back into the toplevel - only to get back the very same answer. In this manner you will learn the more exotic but unavoidable details of Prolog, like quoting, escaping and bracketing. It is practically impossible to learn the syntax otherwise, since Prolog parsers are often extremely permissive.
Your program will be most probably more accessible to declarative reasoning.
Very likely, your two procedures methodone
and methodtwo
are incorrect: You forgot a newline after writing Done
. So methodone, methodone
contains a garbled line. How to test that easily?
But lets look a little bit further into your program. What is so typical for failure driven loops is that they start innocently as something doing "only" side effects but sooner or later they tend to attract more semantic parts as well. In your case, atom_length/2
is hidden down in the failure driven loop completely inaccessible to testing or reasoning.
Prolog systems often implement failure by deallocating a stack. Therefore, failure driven loops will not require a garbage collector. That's why people believe that failure driven loops are efficient. However, this is not necessarily the case. For a goal like findall(A, season(A), As)
every answer for A
is copied into some space. This is a trivial operation for something like atoms but imagine a bigger term. Say:
blam([]). blam([L|L]) :- blam(L). bigterm(L) :- length(L,64), blam(L).
In many systems, findall/3
or assertz/1
for this big term will freeze the system.
Also, systems like SWI, YAP, SICStus do have quite sophisticated garbage collectors. And using fewer failure driven loops will help to improve those systems even further, since this creates a demand for more sophisticated techniques.
If using findall
already, why not maplist
as well:
findall(S, season(S), L), maplist( showseason, L).
Both are not in pure logical Prolog core. And yes, you allocate a whole list for all the solutions.
Your second method is called "failure-driven loop" and there isn't anything wrong with it, except there's no way to get at the previous solutions after backtracking through failure. That's why findall
is extra-logical. Internally, it could be impl'd as failure-driven loop which stores its interim results via asserting. So the second is conceptually cleaner as well, in addition to not allocating any extra memory. It is usually employed in top-level "driver" (i.e., UI) predicates.