Lists are the logic/ASTRA equivalent of an array. However, they operate more like lists provided within languages like Javascript than Java. ASTRA lists are extendable – they do not have a fixed size. Also, they can be both homogeneous (all they values in the list are of the same type) or they can be hetereogeneous (the list contains values that are different types). In this guide, we will explore how to define and maniplate lists.

What you’ll build

You will learn some simple techniques for representing and manipulating lists. Much of the code will be snippets. We suggest that you create a blank project and try each piece of code.

What you’ll need

  • About 15 minutes
  • A favorite text editor or IDE
  • JDK 1.8 or later
  • Maven 3.3+

Declaring Lists

Lists are declared within ASTRA by using a pair of square brackets (“[“, “]”) inside which there is a comma-delimited list of the elements of that list. The example below declares a list of three strings “a”, “b”, and “c”:

  [ "a", "b", "c" ]

Lists can also be used in conjunction with list variables, for example the code below assigns the above list to a variable L:

  list L = [ "a", "b", "c" ];

To append one list to another you use the + operator:

  list L = [ "a", "b" ] + [ "c" ];

The empty list is just a pair of square brackets:

  list L = [];

To store a list in the beliefs of an agent, us define a formula with a term of type list and then add a belief about the list. For example, to declare a belief about a list of events, you could use the format below:

types t {
    formula events(list);
}

initial events([]);

To modify the list, you need to read the belief and then perform an atomic update of that belief. For example, the code below represents a block world type environment where a block is added to the world:

types bw {
    formula blocks([]);
}

initial blocks([]);

rule +block(string X) : blocks(list L) {
    -+blocks(L+[X]);
}

The above code defines an initial empty list of blocks. Whenever the agent handles a belief about a new block, it removes the existing blocks belief and adds a new blocks belief that includes the additional element. Notice that the context condition of the rule is used to retrieve the current blocks belief. The assumption behind this piece of code is that the agent will only ever have a single blocks belief.

In addition to the homogeneous list given above, ASTRA also supports heteregoeneous lists. For example, the list below contains the string “rem”, the integer number, 46, and the functional term “parentOf(…)”.

  [ "rem", 46, parentOf("coral") ]

Heterogeneous lists can be useful for representings things like configurations, they tend to be more difficult to manipulate because the type of each item must be known in advance of its use. However, the manipulation of heterogeneous lists is possible through the use of the Prelude API. We will cover this in a later guide.

For the remainder of this guide, we will focus on homogeneous lists.

Iterating through Lists

Perhaps the main activity associated with lists is iterating through their values. ASTRA includes a number of techniques for doing this. Some are similar to procedural programming techniques (while, for,..). Others are more closely associated with logic programming techniques (the bar operator). FIrst we will explore the procedural techiques and then the logic programming based technique.

The first, and simplest technique is to use a forall statement as is shown below. This only works for homogeneous lists as it allows list values to be of only a single type.

list L = ["a", "b", "c"];

forall (string I : L) {
    console.println(I);
}

If you were to copy this into the +!main(...) rule, then the expected output would be:

[main] a
[main] b
[main] c

The second technique is to use of while loop as is shown below. This technique makes use of two additional operators: the list_count(...) operator returns the size of the list, and the at_index(...) operator returns the value at the given index in the list cast to the given type (below we refer to the value at index i in list L and cast it as a string).

int i=0;
while (i < list_count(L)) {
    console.println(at_index(L, i, string));
    i++;
}

The output is the same as the first technique.

The third technique is to use the bar operator (taken from Prolog). This operator can be used to split a list into its head and its tail. The syntax for the operator is:

[string H | tail T]

It is designed to be used in formulae or events where the full operator is matched against a list which is then decomposed into the head (assigned to H) and the tail (assigned to T). An example of its use is given below:

initial !print(["a", "b", "c"]);

rule +!print([string H | list T]) {
    console.println(H);
    if (list_count(T) > 0) !print(T);
}

Again, the output for this code is the same.

There are a number of variants to this approach. For example, some will use a second rule to stop the recursion:

initial !print(["a", "b", "c"]);

rule +!print([string H]) {
    console.println(H);
}

rule +!print([string H | list T]) {
    console.println(H);
    !print(T);
}

Either of the last two approaches is fine and is equivalent in complexity.

A Larger Example

To illustrate the use of lists, we present a more complex example program. The approach described here has proven quite useful in the design of some practical systems.

Basically, the objective of the code is to provide a way to map lists of activities onto goals that can be invoked by the agent. The list of activities can be modelled as a list of functional terms:

[move("forward"), turn("right"), dust()]

We can pass this list to a goal that extracts each entry in turn and processes it:

rule +!processActivities([funct H, list T]) {
    !processActivity(H);
    if (list_count(T) > 0) !processActivities(T);
}

We can provide a general catch-all rule that will match all cases:

rule +!processActivity(funct A) {
    console.println("Unexpected Activity: " + A);
    system.fail();
}

As can be seen, this activity causes the intention to fail. We can supplement this with additional rules that address specific activities:

rule +!processActivity(move(string dir)) {
    console.println("Moving: " + dir);
}

rule +!processActivity(turn(string dir)) {
    console.println("Turning: " + dir);
}

rule +!processActivity(dust()) {
    console.println("Dusting!!!!");
}

As described in Introduction to AOP with ASTRA, rule ordering is used to determine which applicable rule is selected. This means that the specific rules (described second) must be written before the general rule (described first). The full code for the example is as follows:

agent Main {
    module System system;
    module Console console;

    rule +!main(list args) {
        !processActivities([move("forward"), turn("right"), dust()]);
        system.exit();
    }

    rule +!processActivities([funct H, list T]) {
        !processActivity(H);
        if (list_count(T) > 0) !processActivities(T);
    }

    rule +!processActivity(move(string dir)) {
        console.println("Moving: " + dir);
    }

    rule +!processActivity(turn(string dir)) {
        console.println("Turning: " + dir);
    }

    rule +!processActivity(dust()) {
        console.println("Dusting!!!!");
    }

    rule +!processActivity(funct A) {
        console.println("Unexpected Activity: " + A);
        system.fail();
    }
}

Summary

This guide has explained how lists are represented and manipulated is ASTRA. Three techniques for iterating through a list have been introduced and a worked example that can be used to map elements of a list to goals has been presented.