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.
violates the first constraint. The following rule with the given declaration violates the second constraint.
Given the declaration
the call
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.
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.
{}
{}
{}
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.
{
}
{
}
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.
{
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.
{
{
{
}
}
{
}
}
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
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.
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
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:
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:
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
{
}
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.
{
}
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.