Loops

ASTRA provides three basic approaches for implementing loops. One approach is to use multiple rules to implement a recursive style loop. The second approach is to use a while statement which is equivalent to while loops in imperative programming. Finally, the third approach is to use a foreach statement, which allows you to iterate over all possible bindings of the variables presented in the guard.

Looping using Multiple Rules

The multiple rule-based approach to implementing loops is equivalent to a recursive implementation in imperative programming. Typically, a loop is implemented using 2 rules: 1 for the “loop” body and 1 for the termination of the loop. For example, lets consider printing out the numbers 1 to 5 inclusive. To achieve this, we need a rule that states that if we have not printed out 5 number, then print out the next number and recursively invoke the next iteration of the loop:

agent Loopy {
    module Console console;
    
    initial !print(5);
    
    rule +!print(int X) : X > 1 {
        console.println(X);
        !print(X-1);
    }
    
    rule +!print(int X) : X == 1 {
        console.println(X);
    }
}

The key here is the sub goal statement !print(X-1) and the use of rule contexts. The context of a rule is used to determine whether a rule should be used to handle a given event. Here, two rules are defined that handle the same event, but the context determines which of the rules is applicable for a specific event instance. The sub goal statement causes the looping to occur. Of course, this loop goes from 5 down to 1. To implement the loop in a more traditional way, you simply need to change the context of the rules.

agent UpLoopy {
    module Console console;
    
    initial !print(1);
    
    rule +!print(int X) : X < 5 {
        console.println(X);
        !print(X-1);
    }
    
    rule +!print(int X) : X == 5 {
        console.println(X);
    }
}

The reason why the above code was not presented first is that it is considered to be a worse solution that the first code fragment. The reason why it is viewed as a worse solution is that the number of iterations is hardcoded while in the first code fragment, it was not. To handle variable loop lengths, we need to modify the second example as follows:

agent GoodUpLoopy {
    module Console console;
    
    initial !print(1, 5);
    
    rule +!print(int X, int N) : X < N {
        console.println(X);
        !print(X-1);
    }
    
    rule +!print(int X, int N) : X == N {
        console.println(X);
    }
}

Here, we pass in both the current iteration number and the number of iterations required. You can think of each subgoal as representing iteration X of N where X and N are the variables specified in the event.

Embedding this type of loop in a larger block of code, is relatively trivial:

agent EmbLoopy {
    module Console console;
    module System system;
    
    initial !init();

    rule +!init() {
        console.println("Started program");
        !print(1, 5);
        console.println("Finished Loop");
        system.exit();
    }
    
    rule +!print(int X, int N) : X < N {
        console.println(X);
        !print(X-1);
    }
    
    rule +!print(int X, int N) : X == N {
        console.println(X);
    }
}

As we can see, the loop is triggered by specifying a subgoal in the main plan rule. The main plan blocks until the corresponding subgoal either completes or fails. Also, note here that we have included the System API to allow us to terminate the program using the system.exit() statement.

Looping using While Statements

While loops in ASTRA are basically the same as while loops in any imperative language. Some guard is checked repeatedly, and each time it is evaluated to true, the corresponding statement is executed. When the guard becomes false, the while loop terminates.

Out print example above would be implemented as follows using a while loop:

agent Whiley {
    module Console console;
    
    initial !print(5);
    
    rule +!print(int X) {
        int i = 0;
        while (i < X) {
            console.println(i);
            i = i + 1;
        }
    }
}

To execute the algorithm, you would use similar code to the EmbLoopy example above:

agent EmbWhiley {
    module Console console;
    module System system;
    
    initial !init();

    rule +!init() {
        console.println("Started program");
        !print(5);
        console.println("Finished Loop");
        system.exit();
    }
    
    rule +!print(int X) {
        int i = 0;
        while (i < X) {
            console.println(i);
            i = i + 1;
        }
    }
}

The above approach is a far more natural and appropriate solution for the printing problem, however it can be viewed as being less AgentSpeak-like (basic AgentSpeak does not include while loops). A key difference between the approaches comes from the way in which iterations are executed. In the rule-based approach, iterations are executed as a result of events being generated and handled. These events can be interleaved between other environment / internal events, meaning that there is no guarantee in terms of how long it will take to execute the loop. In contrast, the while-loop approach here does offer more of a guarantee in terms of completion time – because event handling is not used, iterations of the loop are executed consecutively without delay. While this does not give explicit guarantees on performance, it does ensure that the loop will be executed as quickly as possible.

Looping using ForEach Statements

Foreach statements are quite different to the previous two types of loop. Specifically, for each statements are a type of loop where the guard is evaluated only once. The agent then iterates over all the possible variable bindings that have been generated for the guard. This means that the inner statement of the loop cannot affect the number of iterations of the loop. In many senses, this statement can be viewed as plan expansion rather than looping, because it generates one instance of the inner statement per variable binding.

The example below illustrates how Foreach statements work.

agent Eachy {
    module Console console;
    module System system;
    
    initial !init();
    initial rate(1.21);
    initial balance("Rem", 1000.0);
    initial balance("Bob", 500.0);
    
    rule +!init() : rate(double rate) {
        console.println("before loop");
        foreach (balance(string name, double amt)) {
            -balance(name, amt);
            +balance(name, amt*rate);
            console.println(name + " change from: " + amt + " to: " + (amt*rate));
        }
        console.println("after loop");
        system.exit();
    }
}

When the above example is run, the foreach statement generates two variable bindings for the guard: name=“Rem”;amt=1000.0 and name=“Bob”;amt=500.0. It is these two variable bindings that are iterated over, with the following output being generated:

before loop
Rem change from: 1000.0 to: 1210.0
Bob change from: 500.0 to: 605.0
after loop