Terms are the fundamental knowledge representation tool in Plang. Everything, including programs, are ultimately represented as tagged term structures. Terms have one of the following forms:
term --> atom. term --> integer. term --> float. term --> string. term --> variable. term --> functor. term --> list. term --> operator. term --> member_variable. term --> member_call. term --> "(", term, ")".
Atoms are alphanumeric identifiers in the program that represent unique constant values. Examples include:
true
, fail
, foobar
, fooBar
, 'Foo Bar'
, 'hi\nthere'
, person::age
, ''
Atoms have two forms syntactic forms: unquoted and quoted. Unquoted atoms start with a lower case letter, followed by any combination of lower or upper case letters, digits, underscores, and "::" scope markers. Quoted atoms start and end with a single quote and can contain any characters in between.
atom --> lower_letter. atom --> lower_letter, identifier_tail. atom --> "'", squote_string, "'". identifier_tail --> identifier_char. identifier_tail --> identifier_tail, identifier_char. identifier_char --> lower_letter || upper_letter || dec_digit. identifier_char --> "_" || "::". lower_letter --> "a" || "b" || ... || "z". upper_letter --> "A" || "B" || ... || "Z". dec_digit --> "0" || "1" || ... || "9".
The scope marker "::" is a convention only - it does not indicate namespacing in the same sense as C++. Plang uses it in its system library to prefix similar predicates with the module or class name. For example, "person::age" may be the name of a method called "age" within the "person" class, or the name of a global predicate within the "person" module. It is up to the user which interpretation (class or module) makes most sense in their application.
Atoms with the same name are considered identical when compared with (==)/2 or unified with (=)/2.Atoms are not comparable with Strings without first converting them with atom_name/2.
Integers may be represented in decimal, hexadecimal, or octal as in C, with or without a "-" prefix:
42
, 0x2A
, 052
, -42
, -0x2A
, -052
.Note: if a "-" prefix is present, then it must not be followed by whitespace before the digits. The sequence - 42
is a unary functor application, equivalent to '-'(42)
. See the description of (-)/1 for more information.
integer --> dec_integer. integer --> hex_integer. integer --> oct_integer. integer --> "-", dec_integer. integer --> "-", hex_integer. integer --> "-", oct_integer. dec_integer --> dec_digit. dec_integer --> dec_integer, dec_digit. hex_integer --> "0x", hex_digit. hex_integer --> "0X", hex_digit. hex_integer --> hex_integer, hex_digit. hex_digit --> "0" || "1" || ... || "9". hex_digit --> "A" || "B" || ... || "F". hex_digit --> "a" || "b" || ... || "f". oct_integer --> "0", oct_digit. oct_integer --> oct_integer, oct_digit. oct_digit --> "0" || "1" || ... || "7".
Integers can also be expressed as character constants. The constant 0'A'
is equal to the integer 65.
integer --> "0", "'", squote_string, "'". // string must be 1 character
Note: Standard Prolog expresses character constants as 0'A
without a trailing quote. This looks unbalanced and is less obvious as to where a multi-byte character ends.
Floating-point numbers are represented in the usual way:
float --> float_value. float --> "-" float_value. float_value --> dec_integer, ".", dec_integer. float_value --> dec_integer, ".", dec_integer, exponent. float_value --> dec_integer, exponent. exponent --> exponent_char, dec_integer. exponent --> exponent_char, "-", dec_integer. exponent --> exponent_char, "+", dec_integer. exponent_char --> "e" || "E".
Plang only supports double-precision floating-point values as represented by the C double
type on the underlying system.
Numbers with the same value are considered identical when compared with (==)/2 or unified with (=)/2.However, integers and floating-point values are not directly comparable this way; the comparison 3 == 3.0
will fail. Use (=:=)/2 for numeric comparison instead.
Strings typically represent human-readable text for program input and ouput. They start and end with a double-quote character and can contain any other character in-between.
"Hello World!"
, "foobar"
, "\n"
, ""
string --> "\"", dquote_string, "\"". dquote_string --> dquote_char. dquote_string --> dquote_string, dquote_char. squote_string --> squote_char. squote_string --> squote_string, squote_char. dquote_char --> ordinary_char || escape_char || "'". squote_char --> ordinary_char || escape_char || "\"". ordinary_char --> any character except "\"", "'", "\\", or a newline. escape_char --> "\\x", hex_digit, hex_digit. // raw byte value // Unicode characters, which will be converted into UTF-8. escape_char --> "\\u", hex_digit, hex_digit, hex_digit, hex_digit. escape_char --> "\\U", hex_digit, hex_digit, hex_digit, hex_digit, hex_digit, hex_digit, hex_digit, hex_digit. escape_char --> "\\", control_char. escape_char --> "\\", other_char. // same as other_char control_char --> "n" | "r" | "t" | "f" | "v" | "0". other_char --> any character except a control_char.
Strings and atoms are similar in that they both represent textual values. However, their storage is managed differently. When an atom is created, a unique storage area is assigned to the atom that will persist until the program stops executing. This allows atoms to be very quickly compared for equality by simply comparing their pointers. Strings on the other hand are temporary. When there are no further references to the string, the Plang garbage collector will discard the string's storage. The general rule is that program identifiers and constants should be represented as atoms, but program data and human-readable text should be represented as strings.
The atom_name/2 predicate can be used to convert back and forth between atoms and strings.
Variables are alphanumeric identifiers in the program that represent parameters and temporary values during the program's execution. Variable names must start with an upper-case letter or an underscore. Examples include:
A
, FooBar
, _foo_bar
, _
variable --> upper_letter. variable --> upper_letter, identifier_tail. variable --> "_". variable --> "_", identifier_tail.
Variables with the same name that occur in a larger term refer to the same storage. For example, f(X, g(X, Y))
contains two variables X
and Y
. If the variable X was to be bound to the atom a
, then the term would transform into f(a, g(a, Y))
.
The variable named "_" is special in that every occurrence in a term will create a new anonymous variable whose name is unknown. The term f(_, g(_, _))
contains three variables, whereas f(_A, g(_A, _A))
contains only one. The "_" variable is typically used to mark an unused parameter whose value is unimportant to a predicate.
Variable binding is a primary feature of Plang, as in Prolog. Variables are bound during unification to make terms equal. Consider the following code that is using the (=)/2 unification operator:
f(X, g(X, Y)) = f(a, g(Z, W))
When executed, unification attempts to make the two sides of the "=" operator identical. In this case, the following variable bindings will be performed:
X = a Z = a Y = W
If the two sides cannot be made identical then unification fails, the variables bindings are undone, and the program will backtrack to an alternative execution path to find a different solution. The following is an example of a unification that will fail because X cannot be bound to both a
and b:
f(X, g(X, Y)) = f(a, g(b, W))
A variable will keep its bound value until a fail and backtrack occurs. Thus, (=)/2 is not the exactly the same as variable assignment in other programming languages. Subsequent attempts to change the variable to a different value with (=)/2 will fail.
Plang provides separate (:=)/2 and (:==)/2 operators to perform destructive and back-trackable variable assignment in the traditional sense. However, these assignment operators should be used with care as they can break the logical consistency of a solution search.
Functors collect up one or more argument terms and tag them with a functor name. The functor name itself must be an atom. In the following examples, f
and g
are functor names:
f(a)
f(X, g(X, Y))
f(X, f(Y, f(Z, end)))
As can be seen, a functor term is written as an atom, followed by a left-parenthesis, a comma-separated list of argument terms, and a right-parenthesis.
functor --> atom, "(", arguments, ")". arguments --> term. arguments --> "in", term. arguments --> arguments, ",", term. arguments --> arguments, ",", "in", term.
It is typical to refer to a functor by both its atom name and its arity, or number of arguments. The functor f/2 names functor terms whose atom name is f
and which have 2 arguments. The same functor name can have multiple arities, and each arity creates a distinct functor name.
Atoms can be referred to as having "arity 0", especially when they are used to name predicates that do not have any parameters. In Prolog, it is invalid to write a term as name()
, but we allow it in Plang:
functor --> atom, "(", ")". // same as the atom on its own
By convention, the parentheses should only appear after the atom if the atom is being used as the name of a called predicate, such as in the call to print_prompt
below:
mainloop() { do { print_prompt(); read_command(Cmd); ... } while (Cmd != quit); }
Note that the atom quit
was not followed by parentheses, even though it would technically be allowed. That is because the atom is being used as a constant, not a predicate call. Adding parentheses would make the code confusing - it would look like a function call.
Lists are a special kind of functor term for representing organized collections of terms. The following are some examples of lists:
[f(a, X), b, g(Y)]
, [H|T]
, []
[]
is an atom that represents an empty list.
list --> "[", list_members, "]". list --> "[", list_members, "|", term, "]". list_members --> term. list_members --> list_members "," term. atom --> "[", "]".
Lists can also be represented with the (.)/2 functor, although doing so is rare:
'.'(f(a, X), '.'(b, '.'(g(Y), [])))
The for statement can be used to iterate over all members of a list:
for (X in List) {
...
}
The prefix form of functors can be awkward when referring to mathematical operations. The form A + B
is preferable to the prefixed equivalent of '+'(A, B)
. Plang includes a large selection of operators. Unlike Prolog however, the list of operators is fixed by the Plang language. This choice was deliberate, to make it possible to write an efficient parser.
Operators have an associated priority and associativity. The higher the priority, the less tightly the operator binds to its arguments (+ has a higher priority than * for example). Associativity may be one of the following:
xfx
- binary infix operator, non-associative. yfx
- binary infix operator, left-associative. xfy
- binary infix operator, right-associative. fx
- unary prefix operator, non-associative. fy
- unary prefix operator, right-associative.There are presently no unary postfix operators (xf
and yf
associativity) in Plang.
Operator | Priority | Associativity | Example |
(:-)/2, (-->)/2 | 1200 |
|
|
1200 |
|
| |
1130 |
|
| |
1120 |
|
| |
1100 |
|
| |
1050 |
|
| |
1000 |
|
| |
900 |
|
| |
700 |
|
| |
700 |
|
| |
700 |
|
| |
700 |
|
| |
500 |
|
| |
400 |
|
| |
400 |
|
| |
200 |
|
| |
200 |
|
| |
200 |
|
|
Plang also includes some legacy operator names for compatibility with Standard Prolog. The new operator names are the recommended spelling:
Legacy Operator | Priority | Associativity | New Operator |
900 |
| ||
700 |
| ||
700 |
| ||
700 |
| ||
700 |
| ||
700 |
| ||
200 |
|
The (;)/2 and (//)/2 operators from Prolog are not supported in Plang because they clash with ;
used as a statement separator and //
used to start comment blocks. Use (||)/2 and (/)/2 instead.
The term syntax is augmented as follows:
operator --> prefix_operator, term.
operator --> term, infix_operator, term.
If an operator with a higher priority is used as an argument to an operator with a lower priority, then the term must be bracketed:
X is (Y + Z) * W
Plang borrows the concept of prototype-based inheritance from ECMAScript to create an object model. Objects are collections of properties with names and values. A distinguished property called prototype
is used to establish inheritance relationships between objects.
Objects are further subdivided into class objects and instance objects. Class objects are declared using the class keyword:
class vehicle { var wheels } class passenger_car : vehicle { var make, model new(Make, Model) { Self.wheels = 4; Self.make = Make; Self.model = Model; } }
In the above example, the prototype
property on the passenger_car
class object will be the vehicle
class object. The vehicle
class object will not have a prototype
property because it is at the root of an inheritance hierarchy. Class objects also have a className
property. At runtime, the relationships between the class objects can be established as follows:
class(passenger_car, CarClass); class(vehicle, VehicleClass); CarClass.prototype == VehicleClass; CarClass.className == passenger_car; VehicleClass.className == vehicle;
Instance objects are created with the new keyword:
new passenger_car(Car, "MegaCarz", "FastKar 2000"); Car.make == "MegaCarz"; Car.prototype == CarClass; Car.className == passenger_car;
As can be seen, the Car
object instance has inherited the value of the className
property from its class object. This becomes important when we introduce member predicates:
class vehicle { var owner, wheels transferOwnership(NewOwner) { Self.owner := NewOwner; } } Car.transferOwnership("Fred");
The transferOwnership
property is like any other property on the vehicle
class object. If passenger_car
had its own implementation of the transferOwnership
predicate, then it would override the one for vehicle
.
All member predicates, including constructors, are passed a hidden first argument that contains the object that is being operated on. This argument will be available to the member predicate as the variable Self
.
In the examples above we sometimes used (=)/2 to assign values to object properties and other times we used (:=)/2. Why the difference?
When an object is first constructed with new, its var
properties will be set to unbound variables. The constructor can then unify those variables on the Self
object with their initial values.
Later, when we want to change the value of a property to something else we cannot unify it again. Instead, we need to assign a new value using (:=)/2. Because such assignment is destructive, the new value will persist across back-tracking. If you wish the property to automatically revert upon back-tracking, then use (:==)/2 instead.
The only new term syntax that is introduced is for member variable references and member calls:
member_variable --> variable, ".", atom. member_variable --> member_variable, ".", atom. member_call --> member_variable, "(", arguments, ")". member_call --> member_variable, "(", ")".
Note: when "." is used for member variable references, it must not be followed by whitespace. A "." followed by whitespace is intepreted as an end of predicate marker.
See the documentation for the class and new keywords for more information on declaring and creating objects.
Predicates are declared at the top level of a Plang source file, and consist of a head term and an optional predicate body. For example:
parent(fred, mary). parent(mary, bob). ancestor(A, B) { parent(A, B); } ancestor(A, B) { parent(A, C); ancestor(C, B); }
The (:-)/2 operator from Standard Prolog is not supported for predicate definition in Plang, except for dynamic predicates that are created with asserta/1 and assertz/1:
create_rules() { assertz((ancestor(A, B) :- parent(A, B))); assertz((ancestor(A, B) :- parent(A, C), ancestor(C, B))); }
The formal syntax for predicates follows:
predicate --> predicate_head, confidence, ".". predicate --> predicate_head, confidence, compound_statement. predicate_head --> atom. predicate_head --> atom, "(", arguments, ")". predicate_head --> atom, "(", ")". confidence --> []. confidence --> "<<", integer, ">>". confidence --> "<<", float, ">>".
Member predicates within a class can also be declared as static
, abstract
, or instance constructors:
member_predicate --> predicate. member_predicate --> "static", predicate. member_predicate --> "abstract", predicate_head, ".". member_predicate --> constructor_head, confidence, ".". member_predicate --> constructor_head, confidence, compound_statement. constructor_head --> "new". constructor_head --> "new", "(", arguments, ")". constructor_head --> "new", "(", ")".
Abstract member predicates will throw existence_error(member_predicate, Pred)
at runtime if called. This is the same as what will happen if the member predicate is not defined at all. Explicit declaration can be useful for documentation purposes and to override a member in the parent class to redefine it as abstract.
Predicates can be converted into first-class terms using predicate/2.
Constant confidence values, for reasoning with fuzzy logic, can be specified just after the head of a predicate:
cold(10) <<0.7>>.
See also: Input-only arguments, fuzzy_logic.