QuLog Action Rule Subset

Action form the procedural component of QuLog. The definition of a given action is made up of a contiguous sequence of action rules. Action rules take one of the following forms.
RuleHead ~> RuleBody
RuleHead :: RuleGuard ~> RuleBody

In some cases we might want to write a rule that has no action in the rule body. In this case {} represents the null action.

As with function rules, actions are deterministic and cannot fail - in this case a no_matching_action_rule exception will be raised. As discussed in the reference manual, we constrain the use of ? and ?? moded arguments for actions as follows.

  1. Non-variables are not allowed in ? and ?? moded arguments.
  2. Variables that are ! moded are not allowed in ? and ?? moded arguments.
  3. Variables that have already occurred in the body of a rule are not allowed in ? and ?? moded arguments.
For example, the call
read_term([X])

violates the first constraint. The following rule with the given declaration violates the second constraint.

act a(!term)
a(X) ~> read_term(X)

Given the declaration

act read2(??term, ??term)

the call

read2(X, X)

violates the third constraint as the second X occurs earlier (in the first argument).

Typical uses of actions in QuLog are to send and receive messages, update the Belief Store, manage threads and read and write to streams.

We start by giving examples of updating the Belief Store.

act new_child(!human, !age, !human, !human)
new_child(C, A, M, F) ~>
remember([child_of(C,M), child_of(C,F), age_of(C,A)])

act birthday(!human)
birthday(P) :: person(P, _, A) & Z = A+1 & type(Z, age) ~>
forget_remember([age_of(P, A)], [age_of(P, Z)])
birthday(P) ~> write_list([P,
' is not a person or would have an invalid age'])

In the first example we use remember to add facts to the Belief Store (similar to assert in Prolog). In the second we replace an old fact by an updated fact using forget_remember. This is similar to doing a combination of retract and assert in Prolog.

Each call to remember, forget and forget_remember is done atomically, no other thread has either read or write access to the Belief Store until the update is complete. Also when such a call completes a single update to the timestamp on the Belief Store is done and this is used in the TeleoR system to trigger re-evaluation. Furthermore, before the atomic update is complete, any memoized relation or function (see the reference manual) is checked to see if their memoized data needs to be cleared and if so, clears the data.

The next example is an action variant of the relational parser in the examples file. The problem with the relational version is that if the sentence did not parse then the relational parser would simply fail. For the action version below we give feedback output. Because output is an action then this version of the parser needs to be an action.

act do_parse(!string, ?parse_tree)
do_parse(Str,PT) ~>
to_words(Str,Wrds);
write_list(["Word list: ",Wrds,nl_]);
check_dict(Wrds);
write_list(["All words in dictionary",nl_]);
to_parse_tree(Wrds,PT)

act to_words(!string, ?list(string))
to_words(Str,Wrds) :: words(Str,Wrds) ~> {}
to_words(Str,[]) ~>
write_list(['Cannot split into words: ',Str, nl_])

act check_dict(!list(string))
check_dict(Wrds) :: all_dict_words(Wrds) ~> {}
check_dict(Wrds) ~>
write_list(["Unknown words in: ",Wrds, nl_])

act to_parse_tree(!list(string), ?parse_tree)
to_parse_tree(Wrds,PT) :: a_parse_tree(PT,Wrds,[]) ~> {}
to_parse_tree(Wrds,parse_error()) ~>
write_list(['Cannot parse word list: ',Wrds,nl_])

First note that the declaration says that the action must produce a ground term of type parse_tree. Because actions are not allowed to fail, we have added parse_error() to the type enumeration. The problem with this implementation is that if either we cannot tokenize the input or some of the tokenized words are not allowed we continue on to to_parse_tree.

Another option is to declare one or more user declared exceptions and have the action raise one of these exceptions when a problem occurs as in the variant below. This also allows us to terminate early (by raising an exception) as soon as a problem is discovered.

def user_exception ::= cannot_tokenize() |
unknown_words(list(string)) |
cannot_parse(list(string))

act do_parse2(!string, ?parse_tree)
do_parse2(Str,PT) ~>
try {
to_words2(Str,Wrds);
write_list(["Word list: ",Wrds,nl_]);
check_dict2(Wrds);
write_list(["All words in dictionary",nl_]);
to_parse_tree2(Wrds,PT)
}
except {
cannot_tokenize() :: PT = parse_error() ~>
write_list(["Cannot split into words: ",Str, nl_])

unknown_words(Wrds) :: PT = parse_error() ~>
write_list(["Unknown words in: ",Wrds, nl_])

cannot_parse(Wrds) :: PT = parse_error() ~>
write_list(["Cannot parse word list: ",Wrds,nl_])
}

In uses in relation rules forall is used as a test but in action rules it is used for iteration as in the following example.

act remove_child(!human)
remove_child(C) ~>
forall P {child_of(C,P) ~> forget([child_of(C,P)])}

In this example, for a given input child C, we find each parent P of C and forget the child_of fact.

We complete this section by giving a brief explanation of the code for a simpler fact data server that can be updated and queried by any number of QuLog client processes. The code is included near the end of the qlexamples.qlg file.

Clients may update the server's facts by adding and removing facts. They may also query the facts and some of its rule defined relations. We implement the server using a “repeat/fail” approach. The examples file also contains a recursive version.

def message_t ::= tell(dyn_term) | deny(dyn_term)

rel may_update(??dyn_term,!agent_handle)
may_update(age_of(_,_),_)
may_update(child_of(_,_),_)
/* We use the may_update definition to restrict updates
to certain agents. In this case all clients are
allowed to update any age_of or child_of fact.
Such rules may be used to allow only certain clients to update
certain relations, even to restrict updates of certain to facts
having certain argument values. For example we might want
to restrict updates of child_of(_,P) facts to an agent with
handle P@_ - the agent for the parent P. */

% The following relation has a system type declaration
% rel allowed_remote_query_from(??rel_term,!agent_handle)
allowed_remote_query_from(age_of(_,_),_)
allowed_remote_query_from(child_of(_,_),_)
allowed_remote_query_from(person(_,_,_),_)
allowed_remote_query_from(descendant_is(_,_),_)
allowed_remote_query_from(ancestor_is(_,_),_)
/* Any agent is allowed to query without restriction all the
above relations, but only these relations.
We can be more restrictive by partially instantiating the
relation call templates and/or the agent handle arguments. */


act rf_handle_messages()
rf_handle_messages() ~>
fork(rf_handle_message(), Name, messages);
set_default_message_thread(Name)

act rf_handle_message()
rf_handle_message() ~>
repeat {
try {
receive {
tell(Bel) from Ag ::
ground(Bel) & may_update(Bel,Ag) ~>
write_list(["Remembering: ", Bel, nl_]);
remember([Bel])

deny(Bel) from _ ::
nonvar(Bel) & may_update(Bel,Ag) ~>
write_list(["Forgetting:",Bel, nl_]);
forget([Bel])

%% special message pattern for query_at calls
%% from a client
remote_query(ID, QueryStr) from_thread AgTh ::
nonvar(ID) & nonvar(QueryStr) ~>
write_list(["Agent thread ", AgTh,"
asked:", nl_,QueryStr, nl_]);
%% builtin action that parses, type
%% checks, evaluates QueryStr and
%% returns answers to Client
respond_remote_query(ID, QueryStr, AgTh)

M from_thread Addr ~>
write_list(["Invalid message ", M,
" from ", Addr, nl_])
}
}
except {
%% All messages that are received are type checked
%% as a term If the test fails the message is
%% consummed and this exception is raised
input_term_type_error(_, Err) ~>
write_list(["Message type error: ", Err, nl_])
}
}

First we declare a message_t type so that tell and deny terms will be accepted as valid messages. Note that the type says that both of these messages take an argument of type dyn_term that is suitable for remembering and forgetting.

At the top-level, calling rf_handle_messages will fork a thread using the root name of the thread as messages. Assuming no other thread is named messages then Name will be instantiated to messages. We set this thread to be the thread that will receive agent messages - i.e. messages sent using to where the sender doesn't specifier the receiver thread. The created thread will call rf_handle_message.

The top-level of rf_handle_message uses a repeat that causes the thread to repeatedly call the inner action which is a try-except action. We use this in case a client sends a message that is not of type term, which causes an input_term_type_error exception to be raised.

Inside the try we call the receive action which contains a collection of message/address patterns (with an optional guard) and an action. The semantics of the use of receive in this example is as follows.

The call first blocks waiting for a message to arrive. When it does it checks that the message has type term and if not raises an exception. If it is a tell(Bel) message it checks if the message is ground and that the sender of the tell is allowed to update the Bel fact update by querying the may_update relation. If so it prints a message and remembers the sent belief. If instead it is a deny(Bel) message, it checks that Bel is a non-variable, and that the sender may do the update. If so it prints a message and forgets the belief. If the message is a remote_query message then a message is printed and the server parses, evaluates the query whilst checking that for each relation call Call in the query that the querying client is allowed to query the relation of Call in that way by using the allowed_remote_query_from rules in the server. If every call is allowed, it sends back the query answers to the client as a stream of strings. All this is done by the respond_remote_query. If any call in the remote query has no answers, or is not an allowed call for the client which sent the remote query, no answers are returned - a query failure.

Although not used in this case, a receive action can have an optional timeout as the last choice and has the form

timeout Time ~> Action

If no message that matches any of the receive choices has arrived within Time seconds of the call start then Action will be called.

Note that the use of ground and nonvar in the first two choices are necessary as remember requires a ground dyn_term and forget requires a non-variable and hence a dyn_term pattern.

Because receive rejects messages that do not type check it is important that the client and server agree on types otherwise a message term that type checks on the client side might not type check on the servr side and so the message will be rejected. This suggests that the programmer should use a common ontology for both clients and server. One way to do this is to create a file that contains all declarations that are common to both clients and server and consult this file within both the client and server program files.

We now look at examples of sending client messages to the server using the QuLog interpreter - we look the general functionality of the interpreter in more detail in section 8. We assume both the client and server have consulted qlexamples.qlg, and that the server is running on the same machine as the client and has process name server.

| ?? (A :: age_of(june, A)) query_at server.

A = 23 : age

| ?? deny(age_of(june, _)) to server.

success

| ?? tell(age_of(june, 24)) to server.

success

| ?? (A :: age_of(june, A)) query_at server.

A = 24 : age

The first query is an example of remote querying. We are asking the server to find all solutions for the variable A in age_of(june, A). The server returns the list of answers and the client binds A to the first answer and then, on backtracking, binds A to the next solution. The result is the same as the query age_of(june, A) in the server.

The second message causes the server to forget age_of(june, 23) while the third message causes the server to remember age_of(june, 24). The final remote query confirms this change has been made.

Note that we could also use to_thread as below

| ?? tell(age_of(june, 24)) to_thread messages:server.

However, query_at is an agent message rather than a thread message - a special case of to.

A multi info_server example

The directory qulog/examples/introduction/info_broker there are program files for a simple multi sensor information server application. There are two base level sensor information servers, but there can be many more. Acting as an optional query interface to these sensor information servers is a broker agent. The broker agent and the information servers have a shared ontology of three relations describing rooms in a building. The relations give the room temperature, the open or closed status of the room's doors, and which people are in the room.

In the information servers, the rules for the common ontology relations just query dynamic relation facts. For example:

temperature(L,T) <=
temp_info(L,T)
where temp_info is a dynamic relation.

The dynamic relation facts in each information server are being frequently updated by a Python sensor process. In the example you will interact with the Python sensor processes to provide the sense data, but in a real application this Python process would be harvesting readings from sensors in the rooms, sending each changed reading as a message to the information server for which it is the data provider. On receipt of the new reading message the dynamic fact recording the new reading is immediately updated by the information server. So repeated remote queries to the information server return different answers at different times, even though the queries are to 'static' rule defined relations.

The broker agent does not store sensor data. Its dynamic data comprises facts such as:

info_source(temperature, sensor_server1)
giving meta information about which sensor servers are active and may be queried about temperatures. The broker agent's rule defining temperature is:
temperature(Loc, Temp) <=
info_source(temperature, Server) &
temperature(Loc, Temp) query_at Server
This maps a query about temperature to remote queries to each of the sensor servers currently beleived to be an info_source for temperature, i.e. which will have temperature readings for rooms recorded as temp_info dynamic facts.

In the info_broker directory is a README file giving instructions of how to deploy and query this multi-server toy application.

Guarded actions

The action receive of the fact server message handling loop uses a form of guarded actions - in this case the guard is a message pattern together with a test. QuLog supports two other forms of guarded action actions: case and wait_case.

The first of these has the form

case {
Guard1 ~> Action1
...
Guardk ~> Actionk
}

This is similar to a cascading if-then-else in Prolog. If Guardi is the first guard that is true then Actioni will be called. If no guards are true an action_failure exception will be raised.

The second of these has the same form and is a generalization of the wait action.

wait_case {
Guard1 ~> Action1
...
Guardk ~> Actionk
}

The semantics is the same as for case except that if none of the guards are true then wait_case waits until the Belief Store has changed and then re-tests the guards. It only makes sense to use wait_case if the guards either directly or indirectly depend on the Belief Store.

As with receive, wait_case can have an optional timeout as the last choice.