Difference between revisions of "Language Reference/Terms"
m (1 revision(s)) |
|
(No difference)
|
Revision as of 15:22, 15 September 2008
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 BinaryOperator 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 occurence 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: 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.
Example These are examples of unqualified identifiers:
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
Example The built-in domain integer is defined in the global scope, to avoid ambiguity or stress that it is this particular domains you can use the global scope member name:
::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 NamescpacePrefix: NamespacePrefix-opt NamespaceName \ ScopeName: LowerCaseIdentifier
The ScopeName is the name of the class or interface that defines/declares the name.
Classes and interfaces can be organized in namespaces.
When an interface or a class name should be accessed outside of the scope in which this name is declared, then this name can be explicitly qualified the relevant namespace. For example, the interface name interfaceName can be explicitly qualified with some namespace mySpace\subSpace\test\ like this:
mySpace\subSpace\test\interfaceName
Instead of explicit qualification of an interface or a class name with a package name (namespace), you can open the namespace in which this interface or class name is declared (see also openQualification). This will open namespaces inside an interface, class, or class implementation in which it is used. For example,
implement ccc open ns1\yyy\, ns2\zzz\ inherits aaa clauses ... end implement
This open qualification means that namespaces ns1\yyy\ and ns2\zzz\ are opened only inside the implementation of ccc.
When an interface or a class name is declared in one of opened namespaces, then this name can be accessed directly.
However, if such name is declared in two different opened namespaces, then an attempt to use this name directly will lead to ambiguity. To resolve this ambiguity the name can be additionally qualified with the namespace name. For instance, in the previous example we see,
inherits aaa
If two aaa interfaces are declared in two different namespaces ns1\yyy\ and ns2\zzz\, then this string will lead to ambiguity - which of these interfaces is inherited? To resolve this ambiguity the aaa interface name can be additionally qualified with some namespace qualification, like this:
inherits yyy\aaa
Such qualification with yyy\ defines that aaa interface can be one of the following:
yyy\aaa ns1\yyy\aaa ns1\yyy\yyy\aaa ns2\zzz\yyy\aaa
Some names can be accessed without qualification, see Scoping.
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 and expression that evalates 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 reffered to as a function call. A predficate 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.
Example
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.
Example
Consider two terms (of the same type):
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.
Example
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
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.
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. 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 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. unary minus and plus have higher precedence than the power operator, which in turn has higher precedence than the multiplication operators, etc. Parenthesis can be used to circumvent the precedence (and for clarification).
UnaryOperator: one of - +
BinaryOperator: one of PowerOperator MultiplicationOperator AdditionOperator RelationOperator AndOperator OrOperator
The power operator is right associative, all other operators are left associative.
Example
The following term:
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).
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
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 fail, otherwise the "and" term succeeds.
Thus the second sub-term B is only evaluated, if the first sub-term A succeeds.
Example
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.
Notice currently "or" terms can only be used on the outermost level in a clause body, or a goal. Subsequent versions of Visual Prolog will change this matter.
Example
clauses ppp() :- qqq(V), write(V), fail. qqq(X) :- X = 3 ; X = 7.
When ppp is called first call qqq.
qqq will first create a backtrack point to X = 7 and then evaluate X = 3. Thereby X will be bound to 3 and qqq will return. Thereby V in ppp is bound to 3, V(i.e. 3) is written and then fail is met. fail always fail so control is set to the backtrack point in qqq.
Backtracking also undo all bindings made since the backtrack point was created. In this case it means that V in ppp and X in qqq are unbound.
Then X = 7 is evaluated in qqq and X therefore becomes bound to 7, control is returned to ppp and X is bound to 7 and written. And then fail is met again, this time there are no more backtrack points in ppp so ppp fails.
Deeply Nested "or"
It is possible to nest "or" deeply in clauses.
clauses p(X) = Y :- (X = 1, !, Z = 3 or Z = 7), Y = 2*Z.
Here we have used the keyword "or", but you can also use semi-colon ";".
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 and a cut can therefore be necessary in many cases to make the code determ. Especially a cut is needed when using "or" in "if-then-else" constructions:
clauses p(X) :- if (X = 1 or X > 17),! then ... end if.
In future version nondeterministic conditions will be allowed and a cut will be implicitly applied to the condition. This change is backwards compatible, since existing programs will not change behavior.
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.
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.
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: !
Example
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.
Example
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.
Example
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: findall, 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:
member_nd(X, [3,1,2]), if member_nd(X, [3,3]), ! then write(X) else ! end if, fail
member_nd is a nondeterministic predicate. The evaluation of this code will go as follows. First X is bound to 3 and member_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 member_nd, but leaves the backtrack point in the first member_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 member_nd.
member_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 member_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.
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.
Example
[ X || member_nd(X, L), X div 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.
Example
[ X + 1 || member_nd(X, L), X div 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 || member_nd(X, L), X div 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."
List comprehension is a generalization of findall/3. Consider the schematic findall term:
findall(V, Gen, L)
This is equivalent to writing
L = [ V || Gen ]
The main differences between findall and the list comprehension is that findall is a predicate that binds the result to a variable, whereas the list comprehension is an expression.
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.
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.