QuLog Relation Rule Subset

The definition of a given relation is made up of a contiguous sequence of relation rules. Relation rules take one of the following forms.
RuleHead
RuleHead <= RuleBody
RuleHead :: RuleGuard
RuleHead :: RuleGuard <= RuleBody

Normally RuleHead is a simple compound term whose functor is the declared name of the relation and with arity that matches the arity of the corresponding type declaration for the relation. The exception is when we have a higher-order function whose value is a relation. In this case the function is defined via relation rules as in the following example.

fun curryR(rel(T1,??T2)) -> fun(T1) -> rel(??T2),
curryR(rel(T1,?T2)) -> fun(T1) -> rel(?T2),
curryR(rel(T1,!T2)) -> fun(T1) -> rel(!T2)

curryR(Rel)(X)(Y) <= Rel(X,Y)

In either case the head arguments are not allowed to include function calls.

Both RuleGuard and RuleBody are conjunctions of relation calls but that can also include the exists and forall quantifiers. The EBNF in the appendix of the reference manual gives a more exact description of the allowed syntax.

QuLog does not include cut and so we use the guards to commit to rules and so a rule like

p(X) :: q(X) <= r(X)

is semantically the same as the equivalent Prolog rule

p(X) :- q(X), !, r(X)

If we wanted to commit to rule with no body we need to write something like

p(X) :: true

which is semantically the same as the equivalent Prolog rule

p(X) :- !

The examples file examples/introduction/qlexamples.qlg contains many rules for relations (and functions and actions). We discuss a cross section of relation definitions in this file below. The definitions of auxilary relation used below can be found in this file.

First consider

rel person(?human,?gender,?age)
person(H,male,A) <= isa(H,man) & age_of(H,A)
person(H,female,A) <= isa(H,woman) & age_of(H,A)

This is a straightforward definition whose rule bodies are simple conjuctions. The point of interest here is the call on isa in these rules. This, along with type, is a way of doing run time type checking. The main difference is that type is purely a type checker while isa, only used on enumerated or union of enumerated types, can be used as a generator. Notice the the first argument of person is of mode ? and so it is expected that the relation should be able to generate instances of H. If H is a variable at the time of call then isa(H,man) will, on backtracking, bind H to each value in the enumerated type. On the other hand, if H is given it will simply check the type of H.

The next example uses both forall and exists.

rel only_has_adult_children(?human)
only_has_adult_children(P) <=
child_of(_,P) &
forall C (
child_of(C,P) =>
exists A (
person(C,_,A) &
A>20
)
)

The point of this example is to understand the reasons why there is an initial call on child_of and then another call in left hand side of the body of forall and why there is an exists in the right hand side of the body. Both of these have to do with the groundedness of variables.

For both forall and exists we insist that any global variables are ground at the time of call. Further any variables on the right hand side of the body of forall that are not global variables (other than _) are ground by the left hand side. Also, the set of global and quantified variables in a rule are required to be disjoint.

In this example, if we omit the initial call child_of(_,P) then P in child_of(C,P) is a global variable that is not guaranteed to be ground. Secondly, if we omit the exists then A would be treated as a global variable.

The call to forall in this case can be read as “for a given human P then for every child C of P, C has an age A that is greater than 20”.

Since every occurrence of _ is a unique variable it can be though of as implicitly existentially quantified. Indeed, we can modify the above code to replace the inner _ by an explicitely quantified variable as below.

rel only_has_adult_children2(?human)
only_has_adult_children2(P) <=
child_of(_,P) &
forall C (
child_of(C,P) =>
exists G, A (
person(C,G,A) &
A>20
)
)

The above examples were used to illustrate the constraints on global and quantified variables. The variant below is simpler and slightly more efficient and avoids the need to an underscore or extract quantified variable.

rel only_has_adult_children3(?human)
only_has_adult_children3(P) <=
child_of(_,P) &
forall C (
child_of(C,P) =>
exists A (
age_of(C, A) &
A>20
)
)

Negation has similar constraints - global variables inside not need to be ground at call time. The following examples show the use of underscore and exists inside not in order to satisfy the constraint.


rel childless(?human)
childless(P) <=
isa(P, human) &
not child_of(_,P)


rel has_no_siblings(?human)
has_no_siblings(P) <=
isa(P, human) &
not exists Parent, Sibling (child_of(P, Parent) &
child_of(Sibling, Parent) &
P \= Sibling)

The next example gives an example of set comprehension.

rel children_are(?human,?set((age,human)))
children_are(P, Cs) <=
isa(P,human) & Cs = {(A,C) :: child_of(C,P) & age_of(C,A)}

This set comprehension produces a set of age, human tuples. If we replace the parentheses by square brackets we would get a list comprehension (of type list((age,human)).

Set and list comprehension have the same groundedness constraints as forall and exists and explains why the call isa(P,human) is before the comprehension. In this case A and C are quantified variables.

We could use set comprehension to produce a slightly less inefficient version of childless as below.

rel childless(?human)
childless(P) <=
isa(P, human) &
{} = {(A,C) :: child_of(C,P) & age_of(C,A)}

Prolog programmers will see set and list comprehension as a variant of findall, and indeed it is. Python programmers might see this is as similar to a list comprehension - we chose this syntax to make it look more like Python so as to be more readable than using something like findall and also to make it more clear what the quantified variables are.

Python programmers who are not familiar with Prolog might now be wondering about iterators and generators. We don't need to do anything special as backtracking naturally produces one solution at a time as can be seen in the comparative examples below.

We start with a simple Python example - turning a list into an iterator. One way to do this is as follows

def list2gen(lst) :
for x in lst:
yield x

Then we could use this to produce a more complex iterator, for example, with the following expression.

map(lambda x: x**2,
filter(lambda x: x < 8, list2gen([1,6,2,8,5])))

We could then use next to get one value from this iterator at a time with a StopIteration exception raised when there are no more values.

In QuLog we could simply write

X in [1,6,2,8,5] & X < 8 & Y = X**2

and Y would be instantiated to the first solution. On backtracking Y would be instantiated to the next solution. When there are no more solutions we would simply get failure.

Below is a simple example using range to compare an iterator approach in Python with a backtracking approach in QuLog.

In Python we might write

((x,y) for x in range(5) for y in range (5) if x < y)

whereas in QuLog we might write

range(X, 0, 5) & range(Y, 0, 5) & X < Y & Z = (X, Y)

For those not familiar with Python, the above Python expression is a generator and we can ask for the next value as in the following example in the Python interpreter.

gtgtgt g = ((x,y) for x in range(5) for y in range (5) if x < y)
gtgtgt next(g)
(0, 1)
gtgtgt next(g)
(0, 2)
gtgtgt next(g)
(0, 3)

Using next is very similar to backtracking to get another solution in the corresponding QuLog call.

In Python, replacing the brackets with square brackets will produce the list while in QuLog we would write the equivalent list comprehension:

[(X, Y) ::range(X, 0, 5) & range(Y, 0, 5) & X < Y]

We could use list comprehension for list processing as in the following example.

[X**2 :: X in [0,1,2,3,4] & p(X)]

where p is a relation and so p(X) is a test on X. However, because this uses findall, it is less efficient than using straightforward list processing.

With this in mind we have supplied the system defined functions map, filter and filter_map and so the above example is more efficiently written as

filter_map(p, square, [0,1,2,3,4])

where square has been defined as the square function.

The next example shows a use of a run-time type check. The intention is to produce the sum of all the numbers that are in the suplied list of terms.

rel add_nums_of_list_of_any_term(!list(term), ?num)
add_nums_of_list_of_any_term([],0)
add_nums_of_list_of_any_term([N | Rest], Total) ::
type(N,num) <=
add_nums_of_list_of_any_term(Rest, RTotal) &
Total = RTotal+N
add_nums_of_list_of_any_term([ _Any | Rest], Total) <=
add_nums_of_list_of_any_term(Rest, Total)

Since the first argument is a list of terms then any individual element of the list may or may not be a number. The type check type(N,num) as a guard in the second rule guarantees N will be a (ground) number for the body of the rule. Note that the type check is in the guard and not part of the body. If it was in the body then, on backtracking the third rule would also be chosen when N is a number.

The QuLog unification Total = RTotal+N will automatically evaluate the expression argument RTotal+N. This is because, unlike Prolog, QuLog evaluates any function calls in arguments of relation calls before evaluating the relation call.

The next example shows the power of string processing in QuLog. Normally ++ is the builtin string concatination function (fun string ++ string -> string) but when used on the right hand side of =? it turns into a (backtracking) string splitter. We present two versions of a “tokenizer” for English sentences with a sentence terminator. The first version uses backtracking to find words and the second uses regular expressions.

rel words(!string,?list(string))
words(Str,[WStr]) :: Str =? WStr::word(WStr) ++ E::endchar(E)
words(Str,[W|Words]) ::
Str =? W::word(W)++Seps::seps(Seps)++RStr::words(RStr,Words)

rel words2(string, ?list(string))
words2(Str,[WStr]) :: Str =? WStr/"\\w*" ++ _End/"[.?!]"
words2(Str,[W|Words]) ::
Str =?
W/"\\w*" ++
_Seps/"([,;:]?\\s+)|(\\s+)" ++
RStr::words2(RStr,Words)

In the first rule for words we are asking to find a way of splitting the given string Str into two strings WStr and E such that WStr is a word and E is a sentence terminator. If we can find such a split then WStr is the last word of the sentence. In the second rule we see if we can split the sentence into a word, followed by a string of space characters, followed by the remaining sentence (which is recursively processed).

As an example of how this backtracking string matching works consider how the sentence "Hello world!" is processed by this relation.

The first rule is tried but there is no way of splitting up the string into a word followed by a sentence terminator. The second rule is therefore tried.

First W is instantiated to "H" (which is a word and so satisfies the constraint word(W)). However no initial part of "ello world!" is a separator (whitespace). This causes backtracking and W is now instantiated to "He". This backtracking continues until W is instantiated to "Hello". Now Sep is instantiated to " " and we get backingtracking until Seps is instantiated to " ". At this point the remainder of the string, i.e. "world!", is recursively processed. The first rule (after some internal backtracking) will match this string.

The second version uses regular expressions. Instead of using :: constraints we are using a slash followed by a string representing a regular expression. Note the need to double up backslashes. Regular expression matches are deterministic and so they do not create choicepoints.

In general each argument of ++ is one of

VarOrString
VarOrString :: Call
VarOrString / REString
VarOrString / REString :: Call

Often Call is simply a test on Var (which is instantiated to a substring) as in the first rule of words but can be any arbritrary valid call as in the second rule of words where it is a recursive call.

As well as backtracking string processing, =? also works on lists where normally <> is the list appending function but, as with ++, on the right hand side of =? it turns into a backtracking list splitter. The following example, on backtracking, splits a list of numbers at pairs that are in order.

rel split_on_ordered_pair(!list(num), ?list(num), ?num, ?num,
?list(num))
split_on_ordered_pair(Lst, LeftLst, V1, V2, RightLst) <=
Lst =? LeftLst <> [V1, V2] :: (V1 < V2) <> RightLst

In general each argument of <> is one of

VarOrListPattern
VarOrStringListPattern :: Call