Working with Lists

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+

Creating a project from scratch

In a project directory of your choosing, create the following directory structure (for example, on *nix systems, type mkdir -p src/main/astra):

|-- src
|    |-- main
|          |-- astra
|-- pom.xml

Now create the maven build file (pom.xml):

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>examples</groupId>
    <artifactId>lists</artifactId>
    <version>1.4.0</version>

    <parent>
        <groupId>com.astralanguage</groupId>
        <artifactId>astra-base</artifactId>
        <version>1.4.0</version>
    </parent>

    <build>
        <defaultGoal>clean compile astra:deploy</defaultGoal>
        <plugins>
            <plugin>
                <groupId>com.astralanguage</groupId>
                <artifactId>astra-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

You should change the groupId and artifactId to reflect your own project.

Creating a project from an archetype

To use the maven archtetype mechanism to create an ASTRA project, go to a folder of your choice, and type in the following command:

mvn archetype:generate \
    -DarchetypeGroupId=com.astralanguage \
    -DarchetypeArtifactId=astra-archetype \
    -DarchetypeVersion=1.0.0 \
    -DgroupId=examples \
    -DartifactId=lists \
    -Dversion=0.1.0

You should change the groupId and artifactId to reflect your own project.

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, consider an agent assigned the task of stacking labelled blocks (in classic AI, this is known as Blocks World). The code below illustrates how an agent can build a belief about the blocks that are available to it based on the specific blocks it knows to exist:

types bw {
    formula available([]);
    formula block(string);
}

initial available([]);

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

The above code defines an initial empty list of blocks. Whenever the agent observes a new block, a belief will be generated about that block. The rule causes the agent to update its belief about what blocks are available to include the newly discovered block. Notice that the context condition of the rule is used to retrieve the current available belief, and the new belief is added on the first line of the rule body. This is a common pattern in ASTRA code as it leverages the fact that an agent will always execute the first statement in a plan body on the same iteration that the rule is triggered. 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();
    }
}

The expected output of this program, if run is as follows:

[main]Moving: forward
[main]Turning: right
[main]Dusting!!!!

Shuffle and Sort Example

Another example of using lists is the shuffle and sort program below. This program implements code to perform a random shuffle of the elements of a list and then re-sorts the list in ascending order.

package examples;

agent ShuffleSort {
    module Console console;
    module System system;
    module Prelude prelude;
    module Math math;

    rule +!main(list args) {
        list l = [ "a", "b", "c", "d", "e" ];
        !shuffle(l, list l2, 10);
        console.println("shuffled: " + l2);
        prelude.sort_asc(l2);
        console.println("sorted: " + l2);
        system.exit();
    }

    rule +!shuffle(list in, list out, int N) {
        out = in;
        int i=0;
        while (i < N) {
            int j = math.randomInt() % prelude.size(out);
            int k = math.randomInt() % prelude.size(out);
            prelude.swap(out, j, k);
            i = i + 1;
        }
    }
 }
  ```

This program creates an initial list of 5 elements, shuffles them, prints out the shuffled list, sorts the list, and finally prints out the sorted list.  While the output will change every time, it should look something like the output below:

[main]shuffled: ["d","e","c","b","a"] [main]sorted: ["a","b","c","d","e"] ```

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.