Language Reference/Terms
This section describes terms and how execution/evaluation of terms and clauses proceeds.
Semantically, there are two kinds of terms: formulas and expressions.
- Expressions represent values, like the number 7.
- Formulas represent logical statements, like "the number 7 is greater than the number 3".
Syntactically the two kinds have a huge overlap and therefore the syntax unites the two kinds into terms.
The following definition of Term is simplified, in the sense that it includes syntactic constructions that are not legal. For example, one cannot legally write ! + !. We do however believe that using this simple syntax description in combination with intuitive understanding of language concepts, the type system, and the operator hierarchy described below is better for most purposes.
Term: ( Term ) Literal Variable Identifier MemberAccess PredicateCall PredicateExpression UnaryOperator Term Term Operator Term Cut Ellipsis FactvariableAssignment
Backtracking
The evaluation of a Prolog program is a search for a "solution" to the goal. Each step in the search for a solution can either succeed or fail. At certain points in the program execution there are more than one possible choices for finding a solution. When such a choice point is met a so called backtrack point is created. A backtrack point is a recording of the program state plus a pointer to the choice that was not executed. If it turn out that the original choice could not provide the solution (i.e. if it fails), then the program will backtrack to the recorded backtrack point. Thereby restoring the program state and pursuing the other choice. The mechanism will be described and exemplified in details in the following sections.
Literals
Literals have universal type.
Literal: IntegerLiteral RealLiteral CharacterLiteral StringLiteral BinaryLiteral ListLiteral CompoundDomainLiteral
See also Literals (in Lexical Elements)
Variables
Variables in Visual Prolog are immutable: once they are bound to a value they retain that value, but backtracking can unbind the variable again during the process of restoring a previous program state.
A variable can thus be bound (during unification and matching), if it is already bound then it evaluates to the value that it is bound to.
Variables are names starting with an upper-case letter or with an underscore (_), followed by a sequence of letters (both uppercase and lowercase), digits, and underscore characters (all in all called an UppercaseIdentifier):
Variable: UppercaseIdentifer
The following are examples of valid variable names:
My_first_correct_variable_name _ _Sales_10_11_86
while the next two are invalid:
1stattempt second_attempt
The variable consisting of single underscore character (i.e. _) is known as the anonymous variable. The anonymous variable is used in patterns and bindings where the corresponding value is of no interest and should be ignored. Every occurrence of the anonymous variable is an independent anonymous variable, i.e. even though the anonymous variable is used several times in a single clause they have no relation to each other.
If variables that starts with an underscore are not anonymous, but they are still intended for values of no interest that should be ignored. The compiler will issue a warning if the value of such a warning is actually not ignored.
Prolog variables are local to the clause in which it occurs. That is, if two clauses each contain a variable called X, these X-s are two distinct variables.
A variable is said to be free when it is not yet associated with a term and to be bound or instantiated when it is unified with a term.
The Visual Prolog compiler does not make a distinction between upper and lower case letters in names, except for the first letter. This means that the two variables SourceCode and SOURCECODE are the same.
Identifier
Identifier: one of MemberName GlobalScopeMembername ScopeQualifiedMemberName MemberName: LowerCaseIdentifier
Identifiers are used to refer to named entities (i.e. classes, interfaces, constants, domains, predicates, facts, ...).
An identifier can just be a lower case identifier (i.e. a lowercase letter followed by a sequence of letters, numbers and underscore characters).
Many entities can have the same name. So it may be necessary or desirable to qualify the lowercase identifier the name of the particular scope of interest, or to state that the name is in the global namespace.
integer mainExe myPredicate
Global Entities Access
The only global entities, which exist in Visual Prolog, are built-in domains, predicates, and constants. Global names are directly accessible in any scope. There might however exist situations where a global name is shadowed by a local or imported name. In that case the global entity can be qualified with a double colon '::' (without a prefixed class/interface name). The double colon can be used everywhere, but the most important place is where an interface name is used as formal parameter type specifier.
GlobalScopeMemberName: :: MemberName
::integer
Class/Interface Member Access
Static members of classes and interfaces are accessed by means of qualification with the class name (and optionally a namespace prefix):
ScopeQualifiedMemberName NamespacePrefix-opt ScopeName :: MemberName NamespacePrefix: NamespaceIdentifier-opt \ ScopeName: LowercaseIdentifier
The ScopeName is the name of the class or interface that defines/declares the name.
Namespace prefixing is explained in: Referencing names in namespaces.
Some names can be accessed without qualification, see scoping & visibility.
Predicate Call
A predicate call have the form
PredicateCall: Term ( Term-comma-sep-list-opt )
The first term must be an expression that evaluates to a value with predicate type. Typically, it is either the name of a predicate in a class, or an expression that evaluates to a predicate member of an object.
Notice that some predicates return values, whereas other predicates do not. A predicate that returns a value is an expression, and the predicate call is often referred to as a function call. A predicate that does return a value is a formula.
A predicate is invoked by applying arguments to the predicate. The predicate must have a flow-pattern that matches the free/bound state of the arguments.
Most predicates are defined by a set of clauses, but some predicates are built into the language and some are defined externally in a DLL (perhaps in a foreign programming language).
When a predicate is invoked by a predicate call, each clause is executed in turn until one of them succeeds, or there are no more clauses left to execute. If no clause succeeds the predicate fails.
If a clause succeeds and there are more relevant clauses left, a backtrackpoint is created to the next relevant clause.
Thus, a predicate can fail, succeed, and even succeed multiple times.
Each clause has a head and optionally a body.
When a predicate is called the clauses are tried in turn (from top to bottom). For each clause the arguments in the head is unified with the arguments from the call. If this unification succeeds then the body of the clause (if present) is executed. The clause succeeds, if the match of the head succeeds and the body succeeds. Otherwise it fails.
clauses ppp() :- qqq(X), write(X), fail. qqq(1). qqq(2). qqq(3).
When ppp is called it in turn calls qqq. When qqq is called, it first creates a backtrack point pointing to the second clause. Then the first clause is executed. Hereby the free variable X from ppp is matched against the number 1, whereby X is bound to 1.
In ppp X (i.e. 1) is written and then fail cause backtracking to the backtrackpoint. Hereby program control is set to the second clause in qqq and the program state is set back to the state it was in when qqq was first entered, i.e. X in ppp is unbound again.
Before the actual execution of the second clause in qqq begins a backtrack point to the third clause is created. The execution then proceeds as it did for 1
Unification
When a predicate is called the arguments from the call is unified with the terms in the head of each clause.
Unification is the process of binding variables in such a way that two terms become equal, making as few bindings as possible (i.e. leaving as much as possible open for further binding).
Variables can be bound to any kind of terms, including variables or terms containing variables.
Unification is either possible or impossible, i.e. it can succeed or fail.
Variables and terms to which they are unified have types, a variable can only be bound to a term of the same type as the variable, or a subtype. When two variables are bound to each other they must therefore have exactly the same type.
Unification takes place (as mentioned) between a predicate call and the clause head. It also takes place when two terms are compared for equality.
T1 = f1(g(), X, 17, Y, 17) T2 = f1(Z, Z, V, U, 43)
We will attempt to unify these two terms from left to right (i.e. a left-to-right pre-traversal).
Both T1 and T2 are f1/5 terms, this match. Therefore we attempt to unify each of the arguments from T1 with each correspondent argument of T2. First we must unify Z and g(), this can be unified if we bind Z to g(). So far everything is fine and we have the first binding in our unifier:
Z = g()
The next two arguments are X and Z, which already has been bound to g(). These two arguments can also be unified if we also bind X to g(). So we now have the following contributions to our unifier:
X = Z = g()
Next we must bind V to 17 and then we must bind Y to U. So far everything unifies with the following unifier:
X = Z = g() V = 17 Y = U
The two unified terms are now equivalent to these terms:
T1 = f1(g(), g(), 17, Y, 17) T2 = f1(g(), g(), 17, Y, 43)
But we have not yet unified the two last arguments, which are 17 and 43. No variable binding can make these terms equal, so all in all the unification fails.
T1 and T2 cannot be unified.
In the example above T1 could have been a predicate call and T2 a clause head. But they could also have been two terms that were compared with equal "=".
Matching
Matching is the same as unification except that variables can only be bound to grounded terms. A grounded term is a term that does not contain any unbound variables.
It is the flow-patterns that are stated for predicates, that make it possible to use matching rather than full-blown unification.
clauses ppp(Z, Z, 17). qqq() :- ppp(g(), X, 17).
Unification of the ppp-call with the ppp-clause is possible with the following unifier:
Z = X = g()
If ppp have the flow (i,o,i) then the unification is just a match:
- g() is input as the first argument, this is bound to Z
- The second argument in the clause is therefore bound and can thus be output to X, which therefore becomes bound to g().
- finally the third argument is 17 used as input this number is simply compared to the third argument in the clause.
It is the flow-pattern that makes it possible to predict that the clause does not need real unification.
Nested Function Calls
Terms that have to be unified or matched with each other are allowed to contain sub-terms that are actually expressions or function calls that have to be evaluated before the unification/matching can be completed.
The evaluation of such sub-terms is done on a by-need basis.
In a predicate call all input arguments are evaluated before the predicate is called, all output arguments are variables, which does not need evaluation.
Clause heads can also contain terms that have to be evaluated, before matching/unification can be determined.
- all matching/unification that does not require any evaluation is performed before any evaluation is performed;
- then evaluation corresponding to input arguments is performed one by one left-to-right. Comparing each value to the corresponding input after each evaluation;
- then the clause body is evaluated;
- then the output arguments are evaluated (left-to-right);
- then the return value (if the predicate is a function) is evaluated.
If any of these fail then the rest of the evaluation is not carried out.
All in all the base principles are:
- input after other match, before body evaluation
- output after body evaluation
- left-to-right
Arguments
This section describes named, default, optional parameters and functor originals. Even though these notions are individual they also have a significant impact on each other and therefore they are described together.
In brief:
- Named parameters: In a call the actual arguments can be supplied by formal parameter name rather than position.
- Default parameters: A default parameter value can be stated together with a formal argument in a predicate declaration. This value is used when no actual parameter is supplied.
- Optional parameters: An actual parameter is optional and can therefore be skipped.
- Functor originals: Functor term which is used as/bound to the original of another functor term (in a functor term expression).
Named parameters
The formal parameter names from a declaration can be used to specify the actual arguments in a call by using the syntax :<Formal> = <Actual>.
class predicates addPerson : (string Name, integer Height, integer Weight).
You can call addPerson using positional arguments like this:
addPerson("Hans", 175, 75)
But you can also use named parameters like this
addPerson(:Name = "Hans", :Height = 175, :Weight = 75)
Where Name, Height and Weight are the names that is used in the declaration.
When using named parameters the order of the parameters are insignificant, so this call has the same meaning:
addPerson(:Weight = 75, :Name = "Hans", :Height = 175)
Poisitional and named parameters can be mixed such that the first arguments are given by position and the last by name. Here the Name is given by position and Weight and Height are given by name
addPerson("Hans", :Weight = 75, :Height = 175)
Notice that all positional arguments must be to the left of the first named argument.
Default parameters
A default parameter is defined by adding = <value> after a formal input parameter in a declaration (<value> can be a constant expression, a property or predicate call).
class predicates addPerson : (string Name, integer Height = 175, integer Weight = 75).
Height has default value 175 and Weight has default value 75.
Notice that it is illegal to provide default parameters for [out] parameters.
Optional parameters
If a parameter is [out] or if it has a default value, then the actual argument is optional and can be skipped provided that it is the last argument in the call. Skipping an [out] parameter corresponds to supplying an anonymous variable for that parameter. Skipping a parameter that has a default value corresponds to supplying the default value for the parameter.
class predicates ppp : (integer A = 17, integer B [out]).
We can skip B because it is the last parameter and [out], so here we supply 12 for A and ignore the B output:
ppp(12) % corresponding to ppp(12, _)
In the call above 12 is the last argument, but there is a default value for that parameter so that argument can also be skipped:
ppp() % corresponding to ppp(17, _)
Using named arguments we can exchange the order of the parameters:
ppp(:B = Out, :A = 17)
In that call the last parameter has a default value so it can be skipped:
ppp(:B = Out) % corresponding to ppp(B: = Out, :A = 17) --> ppp(17, Out)
An optional out parameter can cause conflict with another predicate:
predicates ppp : (). ppp : (integer X [out]). % conflict
These predicate declarations conflicts because a call without arguments p() could be to either of them.
The attribute [mandatoryOut] can be used to avoid such conflicts and/or if it doesn't seem appropriate that a predicate a has optional output parameters.
predicates ppp : (). ppp : (integer X [out]) [mandatoryOut]. % no conflict
These predicate declarations conflicts because a call without arguments p() could be to either of them.
Functor originals
A functor original is a syntactic construction that describes an original for a functor term expression. Syntactically it takes the following form:
<functor>(<arguments> | <functor original> )
The functor original can either be a a functor value or a free variable. In both cases the construction indicates that the functor may have more arguments than those mentioned explicitly in front of the bar.
If the functor original is a functor value then it will be used as original for constructing a new functor value.
domains person = person(string Name, integer Height, integer Weight). class predicates updateName : (person Person, string NewName) -> person Updated. clauses updateName(Person, NewName) = person(NewName | Person).
Here we use Person as original in the functor term expression person(NewName | Person). This expression will create a new person value which has NewName as the Name/first component. All the remaining functor components (i.e. Height and Weight) are taken from the original, i.e. Person.
The use of functor originals can be combined with named parameters:
domains person = person(string Name, integer Height, integer Weight). class predicates updateWeight : (person Person, Integer NewWeight) -> person Updated. clauses updateWeight(Person, NewWeight) = person(:Weight = NewWeight | Person).
Here NewWeight is used as Weight component. And again the remaining functor components (i.e. Name and Height) are taken from the original, i.e. Person.
The functor original can be any kind of term as long as it evaluates to an appropriate functor term.
clauses mkPerson(Name) = person(Name | getCurrentPerson()).
If a functor domain has more than one alternative, then the functor original must be of same kind as the term that is constructed.
domains transport = aircraft(string Name, integer Seats, integer Range); car(string Name, integer Doors).
The following is legal:
A1 = aircraft("Cruiser 1", 25, 300), A2 = aircraft("Cruiser 2" | A1) % legal A1 is an aircraft
Because A1 is an aircraft, and it is an aircraft we are constructing (as A2).
This is illegal:
C = car("Taxi 23", 4), A = aircraft(:Seats = 25, :Range = 173 | C) % illegal C is a car
Because C is a car but we are constructing an aircraft, it does not matter/help that a car has a Name component, which is what we need for the aircraft we are constructing.
In a functor match the functor original can be an anonymous variable. And in that it simply indicates that the term can have more components which we don't care about.
class predicates getName : (transport Transport) -> string Name clauses getName(aircraft(Name | _)) = Name. getName(car(Name | _)) = Name.
Here we have used an anonymous variable as functor original indicating that the functor term has/may have more arguments (which we are not interested in).
In the car case the functor original only represents one additional argument, but using this form the code will be robust to adding additional components to car later. For the same reason, it can make sense to provide a functor original that doesn't represent any additional arguments (i.e. that represents zero extra arguments).
We could also have used named parameters:
class predicates getName : (transport Transport) -> string Name clauses getName(aircraft(:Name = Name | _)) = Name. getName(car(:Name = Name | _)) = Name.
Here is a predicate that determines whether a transport is a car:
class predicates isCar : (transport Transport) determ. clauses isCar(car( | _)).
This example also shows that it is not necessary to provide any regular arguments at all besides the functor original.
In a functor match the functor original can be a named variable. If the variable is bound then it is used as an original for constructing a functor value and then that functor value is used in the match.
C = car("Taxi 23", 4), car("Taxi 24" | C) = getCurrentCar()
If the functor original is a free variable (in a functor match) then that variable will be bound to the entire functor term:
car("Taxi 24" | Free) = getCurrentCar(),
This code is equivalent to this code:
Free = getCurrentCar(), car("Taxi 24" | _) = Free,
I.e. Free is bound to the result of calling getCurrentCar and then it is matched to this functor pattern car("Taxi 24" | _).
Notice that the code will fail (and not raise an exception) if getCurrentCar returns an aircraft.
Also consider this code (where Name and Car are both free variables):
car(Name | Car) = getCurrentCar(),
it equivalent to this code
Car = getCurrentCar(), car(Name | _) = Car
Fact Variable Assignment
Assign operator := is used to assign a new value for a fact variable FactVariable. The Term must be evaluated to a value of suitable type (i.e. the same type as the fact variable, or a subtype).
FactVariableAssignment: FactVariable := Term
Facts
A fact database contains a number of fully instantiated (grounded) predicate heads corresponding to the facts from the facts section declaration. The facts can be accessed by a predicate call, using the fact name as the predicate name. The predicate call is matched against each fact in turn; succeeding with a possible backtrack point to the next fact each time the predicate call match the fact. When there are no more facts in the fact database then the predicate call fails.
New facts can be asserted using the predicates assert/1, asserta/1, and assertz/1. assert/1 is the same as assertz/1 and it asserts a new fact to the end of the list of facts, whereas asserta/1 asserts a new fact to the start of the list.
Existing facts can be retracted with the predicate retract/1 and retractAll/1. retract/1 retracts the first fact that match the argument binding variables in the argument and leaving a backtrack point so that more facts will potentially be retracted when backtracking.
retractAll/1 retracts all facts that matches the arguments and succeeds without any binding.
Operators
Operators are organized in a precedence hierarchy. In the rule below operators in each group have same precedence, which is higher than those below. I.e. the power operator has higher precedence than unary minus and plus, which in turn has higher precedence than the multiplication operators, etc. Parenthesis can be used to circumvent the precedence (and for clarification).
Operator: one of PowerOperator UnaryOperator MultiplicationOperator AdditionOperator OtherwiseOperator RelationOperator MustUnifyOperator InOperator AndOperator OrOperator
UnaryOperator: one of - +
All operators except the UnaryOperator's are binary. The power operator is right associative, all other operators are left associative.
RelationOperator, MustUnifyOperator and InOperator have same precedence.
Notice that the placement UnaryOperator is not consistent with mathematics, where these operators are at the same level as the AdditionalOperator's. The difference has no influence of the calculated value, but it allows writing 2*-2, where mathematics would require a parenthesis around the second operator 2*(-2). It also means that -2*2 is mmeans (-2)*2 where it would be -(2*2) in mathematics (the resulting value is the same).
-2^2 is the same as -(2^2) because ^ has higher precedence than unary minus.
3^2^2 is the same as 3^(2^2) because ^ is right associative.
-2*-3^-4+5 is the same as ((-2) * (-(3 ^ (-4)))) + 5.
7 + 3 * 5 * 13 + 4 + 3 = X / 6 ; A < 7, p(X)
has the same meaning as this term:
((((7 + ((3 * 5) * 13)) + 4) + 3) = (X / 6)) ; ((A < 7) , p(X))
I.e. at outermost level the term is an "or" of two terms, the first of these is a relational (=) term, the second is an "and" term, etc.
Arithmetic Operators
The arithmetic operators are used for arithmetic operations on numbers. They are expressions, which takes expressions as arguments. They have root types as arguments and return universal types as result. (See Universal and Root types.)
PowerOperator: ^
MultiplicationOperator: one of * / div mod quot rem
AdditionOperator: one of + -
Relational Operators
The relational operators are formulas, which takes expressions as arguments. Given this nature they are non-associative.
RelationOperator: one of = > < >= <= <> ><
First the left term is evaluated, then the right term is evaluated and then the results are compared.
Notice that <> (different) is not the dual operation of = (equal). <> compares two values, whereas = tries to unify two terms (in the general case at least).
The dual to expression A = B is not (A = B).
Must Unify Operator
The must unify operator is a procedure, which takes expressions as arguments. It is non-associative.
MustUnifyOperator: ==
A == B unifies A and B; if the unification fails an exception is raised, otherwise the predicate succeeds. Therefore A == B always succeeds.
clauses p(L) :- [H|T] == L, ...
Bitwise and boolean operators
The following operators for bitwise operations on unsigned, unsigned64 and unsignedNative.
A ** B % A and B A ++ B % A or B A -- B % A and not B A ^^ B % A exclusive or B ~~ A % not A A << N % Shifts the bits in A N times to the left. A >> N % Shifts the bits in A N times to the right.
The logical operations (**, ++, ^^ and ~~) can also be used on boolean values.
The shift operations discard bits that is shifted out of the number, and shift-in zero bits in the opposite end.
Logical Operators
The AndOperator(s) and OrOperator(s) are formulas, which takes formulas as arguments. They are all left associative. The , and and are synonyms and so are ; and or.
AndOperator: one of , and
OrOperator: one of ; or orelse
and (,)
The evaluation of an and term A, B proceeds as follows. First the left sub-term A is evaluated. If this evaluation fails, the whole and term fails. If A succeeds then the right sub-term B is evaluated. If this evaluation fails, the whole and term fails, otherwise the and term succeeds.
Thus the second sub-term B is only evaluated, if the first sub-term A succeeds.
clauses ppp() :- qqq(), rrr().
When ppp is called it will first call qqq and if qqq succeeds, then it will call rrr. If rrr succeeds, then the and term and subsequently the whole clause succeeds.
or (;)
The evaluation of an or term A; B proceeds as follows. First a backtrack point to the second term B is created and then the first term A is evaluated. If the evaluation of the first term succeeds, then the whole or term succeeds and is left with a backtrack to the second term B. If the evaluation of the first term fails, the backtrack point to the second term is activated.
If the backtrack point to the second term B is activated (either because the first term fails, or because something later in the execution invokes the backtrack point), then the second term B is evaluated and the whole or term will succeed if B succeeds.
Thus an or term can succeed with a backtrack point and the second sub-term B is only evaluated on backtrack.
clauses ppp() :- (V = 3 or V = 7), write(V), fail.
Here we have used the keyword or, but you can also use semi-colon ;.
When ppp is called we first create a backtrack point to the term V = 7 and then we evaluate V = 3. Thereby V will be bound to 3 we then continue to the write(V) after 3 has been written fail is met. fail always fails so we effectuate the backtrack point leading us to the term V = 7.
Backtracking also undo all bindings made since the backtrack point was created. In this case it means that V is unbound.
Then V = 7 is evaluated and V becomes bound to 7 and er continue to the term write(V), and then fail is met again, this time there are no more backtrack points ppp fails.
Using parentheses or can be nested deeply in clauses.
clauses p(X) = Y :- (X = 1, !, Z = 3 or Z = 7), Y = 2*Z.
We recommend careful usage of or. It is mainly intended for usage in test-conditions:
clauses isOutside(X) :- (X < 10 or X > 90), !.
or is a nondeterministic construction, but orelse can be used as a deterministic pendant:
clauses isOutside(X) :- X < 10 orelse X > 90.
orelse
orelse is a deterministic pendant to the nondeterministic or. A orelse B will succeed if A succeeds or if B succeeds, but it will not leave a backtrack point to B if A succeeds.
The evaluation of an orelse term A orelse B proceeds as follows: First a backtrack point to the second term B is created and then the first term A is evaluated. If the evaluation of the first term succeeds then the backtrack to the second term (and any backtrack point within it) B are removed again and the whole orelse term succeeds. If the evaluation of the first term A fails, the backtrack point to the second term B is evaluated.
So an orelse term does not leave a backtrack point.
clauses ppp(V) :- (V = 3 orelse V = 7), write(V).
Whenever ppp is called we first create a backtrack point to the term V = 7 and then we evaluate the term V = 3, if V = 3 succeeds we remove the backtrack point to V = 7 again and then continue to write(V). If V = 3 fails the backtrack point to V = 7 is effectuated. If V = 7 succeeds we continue to write(V), if V = 7 fails the entire ppp predicate will fail.
otherwise
otherwise is an expression operator; though it has control flow that makes it resemble the logical operators.
A otherwise B
is an expression (A and B must be expressions). If the evaluation of A succeeds by evaluating to the value VA then A otherwise B evaluates to VA, otherwise (i.e. if the evaluation of A fails) then A otherwise B will be the result of evaluating B.
otherwise is right associative:
A otherwise B otherwise C => A otherwise (B otherwise C)
It has lower precedence than all other expression operators, but higher than relational operators:
V < A + tryGet() otherwise 9 => V < ((A + tryGet()) otherwise 9)
core have been enriched with a predicate isSome for providing default values for core::optional matching:
V = isSome(Optional) otherwise 0
not
The not/1 takes a term as the argument. The evaluation of not(A) first evaluates A. If A succeeds, then not(A) fails, if A fails, then not(A) succeeds.
Notice that not(A) will never bind any variables, because if not(A) succeeds then A has failed, and a failed term does not bind anything. If not(A) on the other hand fails, it cannot bind any variables either, because then the term itself failed.
Also notice that not(A) can never succeed with backtrack points, because if not(A) succeeds then A have failed, and a failed term cannot contain any backtrack points. This in turn means that all possibilities of success in A have been exhausted.
cut (!)
Cut "!" removes all backtrack points created since the entrance to the current predicate, this means all backtrack points to subsequent clauses, plus backtrack points in predicate calls made in the current clause before the "!".
Cut: !
clauses ppp(X) :- X > 7, !, write("Greater than seven"). ppp(_X) :- write("Not greater than seven").
When ppp is executed, there is first created a backtrack point to the second clause, and then the first clause is executed. If the test "X > 7" succeeds then the cut "!" is reached. This cut "!" will remove the backtrack point to the second clause.
clauses ppp() :- qqq(X), X > 7, !, write("Found one"). ppp() :- write("Did not find one"). clauses qqq(3). qqq(12). qqq(13).
When ppp is executed it first creates a backtrack point to the second ppp clause and then qqq is called. The qqq will create a backtrack point to the second qqq clause and execute the first clause, thereby returning the value 3. In ppp variable X is bound to this value and then compared to 7. This test fails and, therefore, the control backtracks to the second clause of qqq.
Before executing the second clause a new backtrack point to the third clause of qqq is created and then the second clause returns 12.
This time the test against 7 succeeds and, therefore, the cut is executed. This cut will remove both the backtrack point left in qqq as well as the backtrack point to the second clause of ppp.
cut Scopes
A cut scope is a scope to which the effect of a cut is limited. Meaning that if a cut is met within a cut scope then only backtrack points within that scope are discarded, while backtrack points outside (i.e. prior to) the cut scope remains.
The clauses of a predicate is a cut scope. Meeting a cut will (at most) discard the backtrack points that was created after entrance to the predicate. Backtrack points created before entrance to the predicate will remain.
clauses aaa() :- p1_nd(), qqq(). qqq() :- p2_nd(), !.
aaa calls p1_nd, which leaves a backtrack point, and then it calls qqq. qqq calls p2_nd, which also leaves a backtrack point. Then we meet a cut. This cut is in the cut-scope of the qqq predicate, so it is only the backtrack point in p2_nd which is discarded, the one in p1_nd remains.
Several terms introduce cut scopes (see the respective terms: list comprehension, if-then-else, foreach). Here we will use if-then-else to illustrate the effect of cut scopes. Consider the schematic if-then-else term:
if Cond then T1 else T2 end if
The condition Cond is a cut-scope, meaning that a cut inside Cond will only have effect inside Cond. Cuts inside T1 and T2, on the other hand, have effect outside the if-then-else statement.
Consider this code fragment:
X = getMember_nd([3,1,2]), if X = getMember_nd([3,3]), ! then write(X) else ! end if, fail
getMember_nd is a nondeterministic predicate. The evaluation of this code will go as follows. First X is bound to 3 and getMember_nd leaves a backtrack point (so that X can later become 1 and then even 2).
Then we evaluate the condition in the if-then-else term. The first part of this condition succeeds as 3 is a member of [3,3]. The first part also leaves a backtrack point, so that it can be examined whether X is a member several times.
Now we meet a cut. This cut is inside the condition part of an if-then-else statement, so it only has local effect, meaning that it only discards the backtrack point in the second getMember_nd, but leaves the backtrack point in the first getMember_nd predicate.
The whole condition succeeds and we enter the then-part and write out "3".
After the if-then-else we meet fail, which backtracks us to the first getMember_nd.
getMember_nd then binds X to 1, and leaves a backtrack point (so that X can later become 2).
Then we evaluate the condition in the if-then-else term. The first part of this condition fails as 1 is not a member of [3,3]. So we enter the else-part.
Here we meet a cut. This cut is in the else-part of a conditional term so it has effect outside the if-then-else term and subsequently it discards the backtrack point in the first getMember_nd.
When we meet the fail after the if-then-else term there are no more backtrack points in the code and it will fail. So all in all X never becomes 2.
fail/0 and succeed/0
fail/0 and succeed/0 are two built-in nullary predicates. fail/0 always fails and succeed/0 always succeeds, besides this the predicates have no effect.
in
InOperator: in
The in operator is used to test for member ship of a collection (e.g. a list) and to nondeterministically generate the members of a collection.
predicates p : (Type X, Type* L). clauses p(X, L) :- if X in L then write("X is in L\n") end if.
p is a procedure that takes a value X and a list L as arguments. If X is in the list L then it will write "X is in L". In this case in is used as an in-test (membership test).
predicates q : (Type* L). clauses q(L) :- foreach X in L do writef("% is in L\n", X) end foreach.
The in operator can be defined for any domain and interface using the in_test and in_iterate attributes.
The in_test(<predicate name>) attribute defines the predicate that is used as in-test for a certain domain or interface. Likewise the in_iterate attribute defines the predicate that is used as in-iterator for the domain/interface.
domains tree{E} = empty; node(tree{E} Left, E Value, tree{E} Right) [in_test(isMemberTree), in_iterate(getAll_nd)].
When the program contains A in B where A is bound B is a tree{E} then isMemberTree is actually called.
In that case A in B corresponds to isMemberTree(A, B).
If A is free the call corresponds to A = getAll_nd(B).For a domain <collection> the predicate must have the type:
predicates <predicate> : (<some-type> Elem, <collection> Collection) determ.
interface collection [in_test(contains), in_iterate(getAll_nd)] ... end interface collection
When the program contains A in B where A is bound B is a collection then contains is actually called.
In that case A in B corresponds to B:contains(A).
If A is free the call corresponds to A = B:getAll_nd().For a domain <collection> the in_test and in_iterate predicate must fulfill these schematic declarations:
domains <collection> = ... [in_test(<in_test>), in_iterate(<in_iterate>)]. class predicates <in_test> : (<some-type> Elem, <collection> Collection) determ. <in_iterate : (<collection> Collection) -> <some-type> Elem nondeterm.
For an interface <collection> the in_test and in_iterate predicate must fulfill these schematic declarations:
interface <collection> [in_test(<in_test>), in_iterate(<in_iterate>)] predicates <in_test> : (<some-type> Elem) determ. <in_iterate : () -> <some-type> Elem nondeterm. ... end interface <collection>
The in operator is predefined on list domains, and in PFC the collections have suitable attributes.
clauses p() :- foreach X in [1, 2, 3, 4] do % in_iterate if X in [2, 4] then % in_test ... end if end foreach.
The first in is the predefined in_iterate for the list domain, and the second one is the predefined in_test.
clauses q() :- M1 = setM_redBlack::new(), M1:inset("a"), M1:inset("b"), M2 = setM_redBlack::new(), M2:inset("b"), foreach X in M1 do % in_iterate if X in M2 then % in_test ... end if end foreach.
For collections the in operators resolve to contains and getAll_nd:
interface collection{@Type} [in_test(contains), in_iterate(getAll_nd)] predicates contains : (@Type Value) determ. % @short Succeeds if the collection contains the value @Type % @end predicates getAll_nd : () -> @Type Value nondeterm. % @short @Type is nondeterministic iteration of the elements in the collection. % @end ... end interface collection
List Comprehension
ListComprehensionTerm : [ Term || Term ]
The list comprehension term is a list expression. Consider this schematic term:
[ Exp || Gen ]
Gen is (typically) a nondeterm term. Exp is evaluated for each solution of Gen, and the resulting Exp's are collected in a list. The Exp corresponding to the first solution of Gen is the first element in the list, etc. This list is the result of the list comprehension term. Exp must be procedure (or erroneous). Both Exp and Gen are cut scopes.
The list comprehension (normally) reads: The list of Exp's such that Gen.
[ X || X = getMember_nd(L), X mod 2 = 0 ]
This reads the list of X's such that X is in L and X is even. So this expression is the even numbers of L.
[ X + 1 || X = getMember_nd(L), X mod 2 = 0 ]
Here the collected expression is more complex. This makes say the term more awkward:
- "the list of (X+1)'s such that ..."
This expression again finds the even elements in L, but the resulting list contains all these values incremented.
This term is completely equivalent to this term:
[ Y || X = getMember_nd(L), X mod 2 = 0 , Y = X+1 ]
This term is easier to say:
- "The list of Y's such that (there exists an) X which is member of L, and which is even, and Y is X+1."
Anonymous Predicates
An anonymous predicate is an expression that evaluates to a predicate value. The predicate value can be bound to a variable, passed as arguments or returned as result, but the value does not have a name in any class, interface or implementation.
Anonymous predicates have the ability to capture values from the context in which the expression occurs, this is a rather powerful ability that can be used to avoid rather excessive amount of strange/unpleasant code.
Syntax
Anonymous predicates are terms:
Term : one of ... AnonymousPredicate ...
An anonymous predicate is a nameless clause in curly brackets. Certain parts are optional, giving these forms:
AnonymousPredicate : one of { ( Arg-comma-sep-list ) = Term } { ( Arg-comma-sep-list ) = Term :- Term } { ( Arg-comma-sep-list ) :- Term } { = Term } { = Term :- Term } { :- Term }
Leaving out the argument list means "the required number of arguments" and can be used whenever the arguments are not used.
Semantics
An anonymous predicate expression evaluates to a predicate value. Consider this code:
clauses run() :- Inc = { (X) = X+1 }, A = Inc(4), B = Inc(23), stdio::writef("A = %, B = %", A, B).
Inc becomes an increment predicate, so the program will write:
A = 5, B = 24
The code in this example corresponds to this code:
clauses run() :- Inc = inc, A = Inc(4), B = Inc(23), stdio::writef("A = %, B = %", A, B). class predicates inc : (integer X) -> integer R. clauses inc(X) = X+1.
Where the clause (X) = X+1 can be found in the last line. I.e. this time in a named predicate.
Variables that are bound outside (i.e. before the occurrence of) an anonymous predicate can be used inside the anonymous predicate. The value of variable will be captured by the anonymous predicate.
Variables that are bound in an anonymous predicate are local variables in the anonymous predicate.
Capturing context
An anonymous predicate can capture context, which means that it can refer to things that are defined in its context, especially facts and variables from the clause.
Capturing Variables
Anonymous predicate occurs in a clause, and this clause may contain variables. Those variables that are bound before the anonymous predicate is met can be used inside the anonymous predicate. This code illustrates how a variable is captured:
domains pred = (integer) -> integer. class predicates createAdder : (integer A) -> pred Adder. clauses createAdder(A) = { (X) = X+A }. clauses run() :- Add17 = createAdder(17), A = Add17(4), B = Add17(20), stdio::writef("A = %, B = %", A, B).
We call createAdder with 17 as argument. So in the createAdder clause A is 17, and therefore the result is { (X) = X+17 }. We say that the anonymous predicate has captured the variable A.
Since Add17 is a predicate that adds 17 to its argument, the output of the code will be:
A = 21, B = 37
Capturing ellipsis (...)
An anonymous predicate can capture the ellipsis variable (i.e. ...):
clauses ppp(...) :- W = { () :- stdio::write(...) }, qqq(W).
W captures the ellipsis variable. qqq receives a zero-arity predicate, when this predicate is invoked the captured ellipsis variable will be written to the standard output device.
Capturing Facts
An anonymous predicate can access facts. If it is created by a class predicate it can access class facts. If it is created by an object predicate it can access both object and class facts. Consider this code that captures a class fact:
class facts count : integer := 0. clauses seq() = { () = count :- count := count+1 }. clauses run() :- A = seq(), B = seq(), stdio::writef("A1 = %, ", A()), stdio::writef("B1 = %, ", B()), stdio::writef("A2 = %, ", A()), stdio::writef("B2 = %", B()).
Both A and B increment the class fact count, so the result is
A1 = 1, B1 = 2, A2 = 3, B2 = 4
In object predicates we can capture object facts. So assuming that seq is an object predicate in myClass, this code illustrates the capture of an object fact:
facts count : integer := 0. clauses seq() = { () = count :- count := count+1 }. clauses run() :- A = myClass::new():seq(), B = myClass::new():seq(), stdio::writef("A1 = %, ", A()), stdio::writef("B1 = %, ", B()), stdio::writef("A2 = %, ", A()), stdio::writef("B2 = %", B()).
In this case A and B comes from two different objects, which each have a count fact, so the output will be:
A1 = 1, B1 = 1, A2 = 2, B2 = 2
Technically, the class version actually doesn't capture anything, it merely have access to the fact. Likewise, the object version doesn't actually capture the fact, instead it captures This and through This it obtains access to the object facts.
Capturing This
As described above it is possible to capture This and thereby gaining access to objects facts. The same mechanism gives access to calling object predicates.
clauses seq() = { () = count :- inc() }. clauses inc() :- count := count+1.
This can also be used directly:
clauses ppp() = { () = aaa::rrr(This) }.
Nesting
Anonymous predicates can be nested:
clauses run() :- P = { (A) = { (B) = A+B } }, Q = P(3300), R = P(2200), stdio::writef("Q(11) = %, ", Q(11)), stdio::writef("R(11) = %", R(11)).
To obtain Q we call P with 3300, so A is 3300 and Q therefore becomes { (B) = 3300+B } }, likewise R becomes { (B) = 2200+B } }. So, the output is:
Q(11) = 3311, R(11) = 2211
Syntactic Sugar
If you don't need the arguments they can be skipped. So this code-fragment:
P = { (_) :- succeed }, Q = { (_, _) = 0 }, R = { (_, _, _) = _ :- fail } Can be shortened down to this: P = { :- succeed }, Q = { = 0 }, R = { = _ :- fail }
Notice that the arguments are completely skipped. If you write () it means zero arguments, whereas skipping the arguments means "a suitable amount" of arguments.
Examples of practical usage
This section shows some cases where anonymous predicates are very handy. The examples assume that the PFC scopes core, std, stdio, list and string are open.
Dummy predicates
Anonymous predicates are good for creating dummy predicate values:
ppp( { = true } ), % don't filter (boolean) qqq( { :- succeed } ), % don't filter (determ) rrr( { = 17 } ), % all rows must have height 17
Adaptation
In cases where you need a predicate and have one that is almost suitable, you can make the adaptation using an anonymous predicate.
Index adaptation
Consider the predicate write3:
class predicates write3 : (function{integer, string} Indexer). clauses write3(Indexer) :- foreach I = std::fromTo(0,2) do write(Indexer(I), "\n") end foreach.
Indexer implements an "array" of strings, write3 will write the three strings found at the indexes 0, 1 and 2. So write3 assumes that the "array" index is zero-based. However, the "array" we have uses a one-based index:
class predicates myArray : (integer N) -> string Value. clauses myArray(1) = "First" :- !. myArray(2) = "Second" :- !. myArray(3) = "Third" :- !. myArray(_) = _ :- raiseError().
But using an anonymous predicate we can easily adapt the one-based array to the zero-based usage:
% myArray is 0-based, write3 requires 1-based Arr = { (N) = myArray(N+1) }, write3(Arr)
So we get the expected output:
First Second Third
Parameter adaptation
In this code listChildren will call a ChildWriter predicate for each "C is the child of P"-pair:
class predicates listChildren : (predicate{string,string} ChildWriter). clauses listChildren(CW) :- CW("Son1", "Father"), CW("Son2", "Father").
We will however prefer to list the "P is the parent of C" using the predicate wParent:
class predicates wParent : (string Parent, string Child). clauses wParent(P, C) :- writef("% is the parent of %\n", P, C).
wParent takes the arguments in the opposite order, but we can easily adapt using an anonymous predicate:
Swap = { (A,B) :- wParent(B,A) }, listChildren(Swap)
And then the out becomes the expected:
Father is the parent of Son1 Father is the parent of Son2
We can also throw away arguments, for example when calling this predicate that only needs a Child:
class predicates wKnowParent : (string Child). clauses wKnowParent(C) :- writef("We know a parent of %\n", C).
The adaptation looks like this:
Fewer = { (C,P) :- wKnowParent(C) }, listChildren(Fewer)
The output will be:
We know a parent of Son1 We know a parent of Son2
We can also supply dummy arguments:
More = { (_,P) :- addChildren(P, 1) } listChildren(More)
Here addChildren will "add a count of children to P". Since each invocation corresponds to one child we will call addChild supplying 1 as a "dummy" argument. The More is thus an adaptor that both throws away an argument and supplies a dummy argument.
Filters
Assume this predicate:
class predicates writeFiltered : (string L, predicate_dt{integer} Filter). clauses writeFiltered(Label, Filter) :- List = [1,2,3,4,5,6,7,8,9], FilteredList = filter(List, Filter), writef("%\t%\n", Label, FilteredList).
Filter is used to filter the list [1,2,3,4,5,6,7,8,9]; the filtered list and the Label are written to the standard output.
First we use the allow-all filter:
All = { :- succeed }, writeFiltered("All", All)
This filter simply succeeds for any element, so the output is the entire list:
All [1,2,3,4,5,6,7,8,9]
It is just as easy to create a filter that fails for all elements and thus allow-none:
None = { :- fail }, writeFiltered("None", None)
The output from this is the empty list:
None []
We can also create filters for elements greater than 3 and elements dividable by 3:
GreaterThan3 = { (X) :- X > 3 }, writeFiltered("> 3", GreaterThan3), Rem3 = { (X) :- 0 = X rem 3 }, writeFiltered("Rem3", Rem3)
The output from this is:
> 3 [4,5,6,7,8,9] Rem3 [3,6,9]
Sorting
The list package has a sort predicate. But sometimes the default order is not what you need. Therefore the list package also has a predicate sortBy, which sorts the elements using a programmer defined compare operation. Let us first consider string sorting, using this predicate:
class predicates writeStringsSorted : (string Label, comparator{string} Comp). clauses writeStringsSorted(Label, C) :- List = ["John Wayne", "Uma Thurman", "Harrison Ford", "Nicolas Cage", "Elizabeth Taylor", "Cary Grant", "Jerry Lewis", "Robert De Niro"], Sorted = sortBy(C, List), write(Label, "\n"), foreach S = list::getMember_nd(Sorted) do writef(" %\n", S) end foreach.
We can call the predicate with the "normal" comparator, and using an anonymous predicate we can easily sort it descending as well:
Normal = compare, writeStringsSorted("Normal", Normal), Descending = { (A,B) = compare(B,A) }, writeStringsSorted("Descending", Descending)
The output looks like this:
Normal Cary Grant Elizabeth Taylor Harrison Ford Jerry Lewis John Wayne Nicolas Cage Robert De Niro Uma Thurman Descending Uma Thurman Robert De Niro Nicolas Cage John Wayne Jerry Lewis Harrison Ford Elizabeth Taylor Cary Grant
Let us also sort some more complex elements. Here a person has a first name and a last name, using this domain:
domains person = p(string First, string Last).
For the demonstration we will use this test predicate:
class predicates writePersonsSorted : (string Label, comparator{person} Comparator). clauses writePersonsSorted(Label, C) :- List = [p("John","Wayne"), p("Uma","Thurman"), p("Harrison","Ford"), p("Nicolas","Cage"), p("Elizabeth","Taylor"), p("Cary","Grant"), p("Jerry","Lewis"), p("Robert","De Niro")], Sorted = sortBy(C, List), write(Label, "\n"), foreach p(F,L) = list::getMember_nd(Sorted) do writef(" % %\n", F, L) end foreach.
Again we can sort using the normal and a descending comparator:
Normal = compare, writePersonsSorted("Normal", Normal), Descending = { (A,B) = compare(B,A) }, writePersonsSorted("Descending", Descending)
Since the compare predicate uses left-to-right lexicographic order on the p-functor, the result is the same as before:
Normal Cary Grant Elizabeth Taylor Harrison Ford Jerry Lewis John Wayne Nicolas Cage Robert De Niro Uma Thurman Descending Uma Thurman Robert De Niro Nicolas Cage John Wayne Jerry Lewis Harrison Ford Elizabeth Taylor Cary Grant
But with the more complex domain we can create a comparator that will sort on last name:
LN = { (p(_,L1), p(_, L2)) = compare(L1,L2) }, writePersonsSorted("LastName", LN)
The result is what we expect:
LastName Nicolas Cage Robert De Niro Harrison Ford Cary Grant Jerry Lewis Elizabeth Taylor Uma Thurman John Wayne
Capturing context
As mentioned a very powerful feature of anonymous predicates is the ability to capture context. The examples in this section show some ways you can use this.
Background threads
The routine for starting a thread takes a null-ary predicate and runs it in the new thread. But you nearly always need to pass some input data to the job in the new thread. This is possible in several ways, but the absolutely simplest way is to use anonymous predicates. The project bgDemo from the Visual Prolog example collection (that can be installed from the IDE) use this method. The project has a form that can start a background job and display status information from the job in a jobControl that is added to the form. A background job is a predicate that will receive a jobLog, which it can use to report status and completion degree:
domains job = (jobLog Log).
A jobLog looks like this:
interface jobLog properties completion : real (i). properties status : string (i). end interface jobLog
The job can report completion degree by setting the completion property (range 0 to 1). Likewise, the status property can be used to reflect the current status of the job.
The status and completion will be shown in the form together with a job name. A job is started by calling the form's addJob predicate:
clauses addJob(JobName, Job) :- JobCtrl = jobControl::new(This), JobCtrl:name := JobName, JobCtrl:show(), assert(jobCtrl_fact(JobCtrl)), arrange(), JobLog = jobLog::new(JobCtrl), Action = { :- Job(JobLog) }, _ = thread::start(Action).
In this context it is the last three lines that are interesting. thread::start takes a null-ary predicate as argument, but a job is a predicate that takes a jobLog as argument. Therefore we create an anonymous predicate Action, which takes no arguments but invokes Job on the JobLog. The anonymous predicate has captured both Job and JobLog from the context, and subsequently both these values are transferred to the new thread even though this thread only receives a null-ary predicate. The jobs in the bgDemo project are merely dummy jobs that only manipulate their jobLog. One of them looks like this:
clauses job(Log, From, To) :- Log:status := "Step 1", foreach N1 = std::fromTo(From, To) do Log:completion := (N1-From) / (To-From) / 2, programControl::sleep(3) end foreach, Log:status := "Step 2", foreach N2 = std::fromTo(From, To) do Log:completion := (N2-From) / (To-From) / 2 + 0.5, programControl::sleep(3) end foreach, Log:status := "finished".
It has two loops which run from From to To and calculates the completion and sets it on the Log. It also sets the status text before, between and after the loops. You may notice that the job does not have the proper job type, because a proper job only has one argument (the jobLog), this job has three arguments. Again it is anonymous predicates that help us. The code that adds the jobs to the form looks like this:
predicates onFileNew : window::menuItemListener. clauses onFileNew(_Source, _MenuTag) :- JF = jobForm::display(This), Job11 = {(L) :- job1::job(L, 1, 1000)}, Job12 = {(L) :- job1::job(L, 200, 600)}, Job13 = {(L) :- job1::job(L, 1200, 3000)}, Job14 = {(L) :- job1::job(L, 1, 1000)}, JF:addJob("job1.1", Job11), JF:addJob("job1.2", Job12), JF:addJob("job1.3", Job13), JF:addJob("job1.4", Job14), ...
In a more realistic program, it is most likely that From and To would not be constants, but rather parameters passed from some outer place. In that case these anonymous predicates would also capture variables from the context. The jobLog in the bgDemo illustrates one more usage of anonymous predicates. The jobLog pass the completion and the status information to a jobControl. The jobControl is a GUI control on the jobForm capable of doing a suitable rendering of the information. This however gives a synchronization problem, because GUI controls are not thread safe and here we want to update some controls from a background thread. This can lead to conflicts, because it is the main thread that draws the controls. The solution is to make transfer the the update of the control to the GUI thread. We do this by posting actions to the control. The implementation of the status update looks like this:
clauses status(Status) :- Action = { :- jobCtrl:status := Status }, jobCtrl:postAction(Action).
Action is a null-ary predicate that will set the status in the jobCtrl. We post this action to the jobCtrl. When the jobCtrl receives the action it invokes it and is thus updated. This way, the actual update of the control will be performed by the GUI thread. This anonymous predicate not only captures the Status variable it also captures the jobCtrl fact.
Asynchronous callbacks
Assume that we send commands to a remote service. The command execution is asynchronous, so when we execute a command we also give a callback action which will be invoked when the execution of the command is finished. To execute a command we must call this predicate:
predicates executeCommand : (command Cmd, predicate{} OnDone).
Based on this predicate we want to create a similar predicate that can execute a list of commands. A certain command should be executed when the previous command completes. We will also make our list executor asynchronous, so we supply an action that will be invoked when the entire script of commands are finished. Our script executer will have the form:
predicates executeScript : (command* Script, predicate{} OnDone).
If the script is empty we simply invoke the OnDone action. If the script has a command H and a rest script T, we must first execute H, and when it is finished we must execute the rest of the script T. So the OnDone action we supply when executing H must execute T. All in all, the implementation can look like this:
clauses executeScript([], OnDone) :- OnDone(). executeScript([H|T], OnDone) :- DoneH = { :- executeScript(T, OnDone) }, executeCommand(H, DoneH).
We have used an anonymous predicate to perform the execution of the rest of the script. This anonymous predicate captures T and OnDone.
Object Member Access
Whenever we have a reference to an object, we can access the object member predicates of that object.
MemberAccess: Term : Identifier
(Currently, the term must be a variable or a fact variable).
The identifier must have the type of the term.
Inside an implementation object member predicates can be invoked without reference to an object, because "This" is subsumed, see Scoping.
Object Expressions
An object expressions is an expressions that evaluates to an object. Like anonymous predicates it can capture values from and access facts in the context it appears in.
The syntax is like a regular class implementation without a name, but with a construction interface:
Object = implement : observer clauses onNext(A) :- F(toString(A)). onCompletion() :- F("\n"). end implement
the implement ... end implement part is an object expression. To make object expressions a lightweight construction the keyword clauses is implicit/optional at the beginning of the scope (unless the scope has open, supports or inherits qualifications).
ObjectExpression: one of FullObjectExpression SimpleObjectExpression FullObjectExpression: implement : ConstructionType ScopeQualifications Sections end implement SimpleObjectExpression: implement : ConstructionType Clause-dot-term-list-opt Sections end implement
There will be exactly one constructor new\0. Like for other objects you don't need to supply an implementation if the constructor is 'trivial'.
inherits, supports, open, delegate and resolve works in the same way as in regular implementations.
Scoping
The predicates can refer to the context like anonymous predicates but can also refer to facts and inherited classes inside the object.
Object expressions are nested within other scopes, these scopes are all visible from the object expression. If a name is defined in more than one surrounding scope then the reference is to the closest level. Names from named scopes can be resolved with the scope name, but names from surrounding object expressions must be resolved by means of a variable as described in the "This" section.
This
In a predicate in an object expression the variable This represent the object of the object expression. There are no predefined names for surrounding This variables. So if you want to refer to an outer This you will have to put it in another variable e.g. Outer = This and then use that variable inside the object expression.
... Outer = This, Object = implement : observer onNext(A) :- F(Outer, toString(A))). onCompletion() :- F(Outer, "\n")). end implement
Polymorphism (limitation)
Occasionally there will be situations where we will need a type variable. An example would be a predicate p : () -> observer{A} implemented using an object expression. Unfortunately, this is not possible until we get access to the type variables of polymorphic constructions.
clauses p(Value) = implement : observer{A} % A is unknown/unbound facts v : A = Value. % A is unknown/unbound clauses onNext(V) :- v := V. onCompleted():- write(v). end implement.
Examples
The examples below will be based on iterator objets.
interface iterator predicates hasNext : () determ. next : () -> integer. end interface iterator
Which we will write using this predicate:
class predicates writeAll : (iterator It). clauses writeAll(It) :- if It:hasNext() then writef("%\n", It:next()), writeAll(It) else write("<<<end>>>\n") end if.
Simple object
class predicates test1 : (). clauses test1() :- It = implement : iterator hasNext() :- fail. next() = _ :- exception::raise_error(). end implement, writeAll(It).
Calling test1 will produce
<<<end>>>
Own state
class predicates test2 : (). clauses test2() :- It = implement : iterator hasNext() :- n > 0. next() = N :- N = n, n := n - 1. facts n : integer := 3. end implement, writeAll(It).
Calling test2 will produce
3 2 1 <<<end>>>
Capturing values
class predicates countDown : (integer From) -> iterator It. clauses countDown(From) = implement : iterator hasNext() :- n > 0. next() = N :- N = n, n := n - 1. facts n : integer := From. end implement. class predicates test3 : (). clauses test3() :- It = countDown(2), writeAll(It).
Calling test3 will produce
2 1 <<<end>>>
class predicates map : (function{integer, integer} Function, iterator It) -> iterator Mapped. clauses map(F, It) = implement : iterator hasNext() :- It:hasNext(). next() = F(It:next()). end implement. class predicates test4 : (). clauses test4() :- It1 = countDown(2), It2 = map({ (V) = V + 7 }, It1), writeAll(It2).
Calling test4 will produce
9 8 <<<end>>>
Capturing a fact
class facts count : integer := 3. class predicates test5 : (). clauses test5() :- It = implement : iterator hasNext() :- count > 0. next() = N :- N = count, count := count - 1. end implement, writeAll(It).
Calling test5 will produce
3 2 1 <<<end>>>
inherits
We can inherit from this iterator to create a iterator that "throws a dice":
class predicates test6 : (). clauses test6() :- Dice = implement : iterator inherits randomIterator clauses next() = randomIterator::next() mod 6 + 1. end implement, foreach _ = std::cIterate(5) do writef("Dice : %\n", Dice:next()) end foreach.
By inheriting randomIterator we get the implementation of hasNext and only need to implement next.
Notice that it is necessary to use the clauses keyword in this case because the class has an inherits qualification.
Assuming that randomIterator will never end, we just iterate 5 times.
Calling test5 will produce (something like):
Dice : 1 Dice : 2 Dice : 4 Dice : 2 Dice : 3
Domain, Functor, and Constant Access
Domains, functors, and constants are all accessed as if they are class members. Even if they are declared in an interface. This means that when they are qualified, then they are always qualified with class/interface name and a double colon.
foreach
ForeachTerm : foreach Term do Term end foreach
Consider the schematic foreach term:
foreach Gen do Body end foreach
Gen is (typically) a nondeterm term. Body is evaluated for each solution of Gen. If/when Gen fails the foreach-term succeeds without evaluating Body. Body must be procedure (or erroneous). Gen and Body are both cut scopes.
The schematic foreach term resembles a fail loop:
Gen, Body, fail
The main (and important) difference is that a foreach-term succeeds after the iteration, whereas a fail loop fails. As a result foreach-terms can be followed by other terms and they can be properly nested.
foreach L = list::getMember_nd(LL) do write("<< "), foreach X = list::getMember_nd(L) do write(" ", X) end foreach, write(" >>\n") end foreach.
LL is supposed to be a list of lists. The outer foreach-loop iterates L through this list, so each L is a list. The inner foreach-loop iterates X through the elements of L.
There are a number of things to notice:
- There is no comma before the keywords "do" and "end foreach".
- The Body of a foreach-term is a cut scope, so a cut meet in the body will not influence the iteration.
- A foreach-term always succeeds (or raises an exception), and no extra variables are bound after the evaluation of a foreach-term. Therefore, a foreach-term can only be used for its side effects.
clauses p(L) :- foreach X = list::getMember_nd(L) do stdio::write(X, "\n") end foreach, stdio::write("Finished\n").
Foreach can be used instead of traditional "fail-loops":
clauses p(L) :- X = list::getMember_nd(L), stdio::write(X, "\n"), fail. p(_L) :- stdio::write("Finished\n").
In this context it is advantageous that Body must be a procedure, because in a "fail-loop" the body may accidentally fail before reaching the fail in the loop. Another advantage is that a foreach loop succeeds when the loop is finished, whereas a fail loop fails, so that execution can continue in the same clause. Also notice that foreach can be properly nested:
clauses p(LL) :- foreach L = list::getMember_nd(LL) do foreach X = list::getMember_nd(L) do stdio::write(X, "\n") end foreach, stdio::write("Finished a list\n") end foreach, stdio::write("Finished all lists\n").
if-then-else
if-then-else can be used both as a statement and as an expression.
if-then-else (statement)
The if-then-eslse statement conditionally executes a group of statements.
IfThenElseTerm: if Condition then Term Elseif-list-opt Else-opt end if Elseif: elseif Condition then Term Else: else Term
The following two terms are equivalents.
if Cond1 then T1 elseif Cond2 then T2 else T3 end if
if Cond1 then T1 else if Cond2 then T2 else T3 end if end if
Consider the schematic if then else term:
if Cond then T1 else T2 end if
First Cond is evaluated, if it succeeds then T1 is evaluated otherwise T2 is evaluated.
Cond is followed by an implicit cut, which turns:
- a nondeterm condition into a determ condition and
- a multi condition into a procedure.
Cond is a cut-scope (see Cut Scopes).
clauses w(X) :- if X div 2 = 0 and X > 3 then write("X is good") else write("X is bad"), nl end if, nl.
There are several things to notice about this example:
- You can use "and" and "or" logical operators and other "complex" terms in all three sub-terms.
- There is no comma before the keywords "then", "elseif", "else", and "end if".
For readability sake, we always recommend using "or" instead of ";". Likewise we also recommend using "and" (instead of ",") when it (as in the condition above) represents a logical "condition" rather than a "sequentation".
Leaving out the else-part is just shorthand for writing that else succeed, i.e.
if Cond then Term end if
is short-hand for
if Cond then Term else succeed end if
if-then-else (expression)
The if-then-else expression conditionally evaluates expressions.
Syntactically it is same as the if-then-else statement, but and the terms in the branches must be expressions and the entire if-then-else expression will itself evaluate to a value.
The shorthand writings that leave out the else-part does not make sense for the expression.
clauses w(X, Y) :- Min = if X < Y then X else Y end if, writef("The minimum is : %\n", Min).
try-catch-finally
The try-catch-finally statement provides means for dealing with exceptions that may occur in a given block of code.
TryCatchTerm: try Term CatchFinally-list end try CatchFinally: one of catch Variable do Trap-Handler finally Finally-Handler Handler: Term
A try-construction thus have a Term and a list of catch and finally handlers.
The Term and the handlers are not allowed to leave backtrack points (i.e. they cannot have mode multi or nondeterm).
A try-construction with more than one handler is equivalent to nesting several try-constructions with one handler each. I.e. the following term:
try Body catch Var1 do Handler1 finally Handler2 catch Var2 do Handler3 end try
Is equivalent to these nested terms:
try try try Body catch Var1 do Handler1 end try finally Handler2 end try catch Var2 do Handler3 end try
try-catch
Consider the try-catch construction
try Body catch Var do Handler end try
First Body is evaluated.
If Body fails or succeeds the whole try construction fails or succeeds, respectively. I.e. if Body does not terminate with an exception the try construction corresponds to evaluating Body.
If Body terminates with an exception, then the exception is caught by the catch handler. Meaning that first Var is bound to the exception (in PFC context the exception will be a traceId) and then Handler is evaluated. In this case the construction behaves as if Handler is evaluated with Var bound to the caught exception.
Notice, that no bindings are made by Body if it terminates with an exception.
clauses read(Filename) = Contents :- try Contents = file::readFile(Filename) catch TraceId do stdio::writef("Could not read the file: %\n", Filename), exceptionDump::dumpToStdio(TraceId), Contents = "" end try.
First Contents = file::readFile(Filename) is evaluated, if that does not terminate with an exception read returns the contents of the file.
If it raises an exception the handler is evaluated with TraceId bound to the exception. So in this case a message is written to stdio and then the exception is dumped to stdio, and finally Contents is set to the empty string which is returned as result.
try-finally
Consider the try-finally construction:
try Body finally Handler end try
The purpose of the construction is to evaluate the Handler after the Body no matter how the Body terminates, i.e. whether it succeeds, fails or terminates with an exception (it cannot leave backtrack points).
The evaluation is like this
First Body is evaluated.
- If Body succeeds: Handler is executed and the try-finally construction succeeds.
- If Body fails: Handler is executed and the try-finally construction fails.
- If Body terminates with an exception Exn: Handler is executed and the try-finally construction terminates with the exception Exn.
clauses tryGetName(Connection, PersonId) = Name :- Stmt = odbcStatement::new(Connection), try Stmt:setParameterValue(1, core::integer(PersonId)), Stmt:execDirect("select name from person where id=?"), Stmt:fetch(), Name = Stmt:getParameterValue_string(1) finally Stmt:free() end try.
The Stmt is free'd also if fetch fails, or if something terminates with an exception.