4.2.3.5 Do-Loops

Proposed in [Schimpf 2002], the control structure

     (Iterators do Body)

often eliminates the need to write an auxiliary predicate to perform some simple iteration. A do-loop is substituted by a goal:

     PreCallGoals, aux(CallArgs).

where aux is a new, unique predicate symbol, CallArgs is its initial arguments, and PreCallGoals is a sequence of goals to be executed before calling aux. In addition, a definition for aux is defined, and is always of the form:

     aux(BaseArgs) :- !.
     aux(HeadArgs) :- PreBodyGoals, Body, aux(RecArgs).

where BaseArgs, HeadArgs and RecArgs are sequence of arguments and PreBodyGoals a sequence of goals.

The ‘do’ operator is an infix operator of the same priority as ‘;’. It is recommended to always enclose a do-loop in parentheses.

Iterators is a comma-separated sequence of iterators. Before giving the full list of available iterators, we first show some simple examples.

The iterator foreach(Var,List) provides iteration over a list:

     | ?- (foreach(X,[1,2,3]) do write(X), nl).
     1
     2
     3
     yes

The same iterator can be used to construct a list:

     | ?- (foreach(X,[1,2,3]), foreach(Y,List) do Y is X+3).
     List = [4, 5, 6]

The iterator fromto(First,In,Out,Last) can be used to express an accumulator with initial value First, final value Last, with In and Out being local variables in Body:

     | ?- (foreach(X,[1,2,3]), fromto(0,In,Out,Sum) do Out is In+X).
     Sum = 6

The iterator for(Var,Min,Max) will iterate Body with Var ranging over integers Min thru Max, which can be expressions:

     | ?- (for(I,1,5), foreach(I,List) do true).
     List = [1,2,3,4,5]

The iterator count(Var,Min,Max) will iterate Body with Var ranging over ascending integers from Min, unifying Max with the final value. Its main use is to count the number of iterations:

     | ?- (foreach(X,[a,b,c,d,e]), count(I,1,N), foreach(I-X,Pairs) do true).
     N = 5,
     Pairs = [1-a,2-b,3-c,4-d,5-e]

The iterator foreacharg(Var,Struct) provides iteration over the arguments of a structure. The variant foreacharg(Var,Struct,I) also exists, with I ranging over the argument number, 1-based:

     | ?- (foreacharg(A,f(1,2,3)), foreach(A,List) do true).
     List = [1,2,3]
     
     | ?- (foreacharg(A,f(a,b,c,d,e),I), foreach(I-A,List) do true).
     List = [1-a,2-b,3-c,4-d,5-e]

Do-loops have special variable scoping rules, which sometimes contradict the default rule that the scope of a variable is the clause in which it occurs: the scope of variables occurring in Body as well as variables quantified by iterators is one loop iteration. The exact scope of variables is given in the table below. To override the scoping rule, i.e. to enable a variable to be passed to all loop iterations, use the param(Var) declaration:

     | ?- (for(I,1,5), foreach(X,List), param(X) do true).
     List = [X,X,X,X,X]

An omitted param(Var) iterator is often spotted by the compiler, which issues a warning. Suppose that we want to define a predicate that removes all occurrences of the element Kill from the list List giving Residue. A do-loop formulation is given below, along with a buggy version where param(Kill) is missing:

                                    
% do.pl
delete1(List, Kill, Residue) :- % correct ( foreach(X,List), fromto(Residue,S0,S,[]), param(Kill) do (X = Kill -> S0 = S ; S0 = [X|S]) ). delete2(List, Kill, Residue) :- % wrong ( foreach(X,List), fromto(Residue,S0,S,[]) do (X = Kill -> S0 = S ; S0 = [X|S]) ).

The compiler warns about the missing param(Kill), and for a good reason: the first version works as indended, but the second does not:

     | ?- [do].
     % compiling /home/matsc/sicstus4/do.pl...
     * [Kill] treated as local in do-loop but also used outside
     * suggest renaming or adding param([Kill])
     * Approximate lines: 8-15, file: '/home/matsc/sicstus4/do.pl'
     % compiled /home/matsc/sicstus4/do.pl in module user, 10 msec 192 bytes
     | ?- delete1([1,2,3,4,5], 3, R).
     R = [1,2,4,5]
     
     | ?- delete2([1,2,3,4,5], 3, R).
     R = []

Finally, do-loops can be used as a control structure in grammar rules as well. A do-loop in a grammar rule context will generate (or parse) the concatenation of the lists of symbols generated (or parsed) by each loop iteration. For example, suppose that you are representing three-dimensional points as lists [x,y,z]. Suppose that you need to generate a list of all such points for x between 1 and Length, y between 1 and Width, and z between 1 and Height. A generator of such lists can be written as a grammar rule with nested do-loops as follows.

     | ?- compile(user).
     | points3d(Length, Width, Height) -->
     |         (   for(X,1,Length),
     |             param(Width,Height)
     |         do  (   for(Y,1,Width),
     |                 param(X,Height)
     |             do  (   for(Z,1,Height),
     |                     param(X,Y)
     |                 do  [[X,Y,Z]]
     |                 )
     |             )
     |         ).
     | ?- ^D
     % compiled user in module user, 0 msec 1024 bytes
     | ?- phrase(points3d(3,2,4), S).
     S = [[1,1,1],[1,1,2],[1,1,3],[1,1,4],
          [1,2,1],[1,2,2],[1,2,3],[1,2,4],
          [2,1,1],[2,1,2],[2,1,3],[2,1,4],
          [2,2,1],[2,2,2],[2,2,3],[2,2,4],
          [3,1,1],[3,1,2],[3,1,3],[3,1,4],
          [3,2,1],[3,2,2],[3,2,3],[3,2,4]]

We now summarize the available iterators. In this table, the phrase “var is a local variable” means that var is a brand new variable in each iteration. All other variables have global scope, i.e. the scope is the clause containing the do-loop.

fromto(First,In,Out,Last)
Iterate Body starting with In=First until Out=Last. In and Out are local variables. For all but the first iteration, the value of In is the same as the value of Out in the previous iteration.


foreach(X,List)
Iterate Body with X ranging over all elements of List. X is a local variable. Can also be used for constructing a list.


foreacharg(X,Struct)
Iterate Body with X ranging over all arguments of Struct. X is a local variable. Cannot be used for constructing a term.


foreacharg(X,Struct,Idx)
Same as before, but Idx is set to the argument position of X in Struct, i.e. arg(Idx, Struct, X) is true. X and Idx are local variables.


for(I,MinExpr,MaxExpr)
Iterate Body with I ranging over integers from MinExpr to MaxExpr. I is a local variable. MinExpr and MaxExpr can be arithmetic expressions. Can be used only for controlling iteration, i.e. MaxExpr cannot be uninstantiated.


count(I,Min,Max)
Iterate Body with I ranging over integers from Min up to Max. I is a local variable. Can be used for controlling iteration as well as counting, i.e. Max can be a uninstantiated.


param(Var)
For declaring variables global, i.e. shared with the context, even if they are quantified by some other iterator of this table. Var can be a single variable or a list of variables. Please note: By default, variables in Body have local scope.


IterSpec1, IterSpec2
The specifiers are iterated synchronously; that is, they all take their first value for the first execution of Body, their second value for the second execution of Body, etc. The order in which they are written does not matter, and the set of local variables in Body is the union of those of IterSpec1 and IterSpec2. When multiple iteration specifiers are given in this way, typically not all of them will impose a termination condition on the loop (e.g. foreach with an uninstantiated list and count with an uninstantiated maximum do not impose a termination condition), but at least one of them should do so. If several specifiers impose termination conditions, then these conditions must coincide, i.e. specify the same number of iterations.

Finally, we present a translation scheme for the iterators in terms of PreCallGoals, CallArgs, BaseArgs, HeadArgs, PreBodyGoals and RecArgs, as previously introduced:

iterator PreCallGoals CallArgs BaseArgs HeadArgs PreBodyGoals RecArgs

fromto(F,I0,I1,T) true F,T L0,L0 I0,L1 true I1,L1

foreach(X,L) true L [] [X|T] true T

foreacharg(A,S) functor(S,_,N), S,1,N1 _,I0,I0 S,I0,I2 I1 is I0+1, S,I1,I2
N1 is N+1 arg(I0,S,A)

foreacharg(A,S,I1) functor(S,_,N), S,1,N1 _,I0,I0 S,I0,I2 I1 is I0+1, S,I1,I2
N1 is N+1 arg(I0,S,A)

count(I,FE,T) F is FE-1 F,T L0,L0 I0,L1 I is I0+1 I,L1

for(I,FE,TE) F is FE F,S L0,L0 I,L1 I1 is I+1 I1,L1
S is max(F,TE+1)

param(P) true P P P true P


Send feedback on this subject.