To execute a Daki program you must use a Daki language interpreter. You can find on GitHub the reference interpreter (mirror).
Regardless of your familiarity with Prolog or Datalog, the Daki language has significant differences to both. For this reason the following short language definition has been compiled, with examples.
Contents
- Introduction
- Comments
- Declarations
- Queries
- Retractions
- Environment commands
- Operator clauses
- Arithmetic operator clauses
- Bitwise operator clauses
- Equality/order operator clauses
- Type casting operator clauses
- String operator clauses
- List operator clauses
- Other operator clauses
- Clause conditions
- Inline Operations
- Memoization
- Copyright notice
Introduction
The Daki language is meant to be read by a computer. The usual flow of a Daki language computer program is to read a collection of declarations, and then allow the user to interact with the knowledge base and make questions to be solved via inference.
One interesting quality about Daki is that it is meant to bridge the capabilities of Datalog with some of the more powerful features of Prolog. In Datalog queries return all solutions, while in Prolog by default only the first solution is returned. In Daki you can always easily select between the two query methods, and still have access to powerful features missing from Datalog, like a typed system and automatic memoization. There are many other differences from these languages to Daki; these are introduced where appropriate along this text.
There are only five types of instructions in Daki, and these cannot be mixed (except for comments that may be inlined):
- Comments
- New declarations
- Queries
- Declarations to be removed
- Built-in commands
Comments
Technically not an instruction, because they are not interpreted; comments start with the "#
" character, and everything after this character is ignored by the interpreter.
> # I am a comment > > fact('john', 'mary', 1). # I'm a comment too
Declarations
New declarations add what is called a clause to the global table of clauses (sometimes called database or knowledge base in other logic languages). A clause is composed of a head declaration and an optional tail, separated by the characters ":-
".
> parent('john', 'emily'). > grandparent(A, B) :- parent(A, C), parent(C, B).
Clauses are always terminated by a dot, ".
". If they are declared with a tail, the tail must be evaluated true for the head to match. Clauses with a tail are called rules, while clauses without it are called facts.
In Daki, the tail dependencies order is not important. In accordance with other logic languages, the ",
" character is used to denote logical AND, and the ";
" character logical OR. Notice how these are equivalent in Daki:
> rule(x) :- reason1(x); reason2(x). > # is for all purposes the same as > rule(x) :- reason1(x). > rule(x) :- reason2(x).
In Daki, all clauses have their tail dependencies resolved into clauses with only logical AND relationships, and it is in this format they are stored internally. For example:
> d(X, Y, X) :- (e(X); e(Y); e(Z)), (f(X); f(Y); f(Z)). > listing 0: d(X, Y, X) :- e(X), f(X). 1: d(X, Y, X) :- e(X), f(Y). 2: d(X, Y, X) :- e(X), f(Z). 3: d(X, Y, X) :- e(Y), f(X). 4: d(X, Y, X) :- e(Y), f(Y). 5: d(X, Y, X) :- e(Y), f(Z). 6: d(X, Y, X) :- e(Z), f(X). 7: d(X, Y, X) :- e(Z), f(Y). 8: d(X, Y, X) :- e(Z), f(Z).
Notice how you can use parenthesis to declare many logical AND relationships at once.
The elements of clauses always have open brackets and are declared with one or more strings. Those strings can be literals - with a specific data type - or variables.
Data types
Literals in Daki always have an implicit data type associated. Many operations are only possible between compatible data type literals: this will be expanded on later in this document.
The Daki data types are string, for Unicode strings (for example 'daki'
), integer, for arbitrary size integers (42
), float, for IEEE 754 floating point numbers (3.14
) and list, for sequential lists of literal values of arbitrary length and depth (['john', 24, ['skiing', 'running']]
). This document also uses the term numeric to mean both integer and floating point values.
Literal values are not automatically coerced or matched, for example:
> value('1', 1). > value(1, 2). > value(1.0, 3). > value([1], 4). > > value('1', X)? X = 1 > value(1, X)? # integer value 1 did not unify with floating point 1.0 X = 2 > value(1.000, X)? # integer value 1 did not unify with floating point 1.0 X = 3 > value([1], X)? X = 4
Integer literals can be specified in decimal, octal, hexadecimal or binary notation:
> value(119). # decimal > value(0170). # octal > value(0x7a). # hexadecimal > value(0b1111011). # binary > listing 0: value(119). 1: value(120). 2: value(122). 3: value(123).
String literals are UTF-8 encoded Unicode strings that can be enclosed both by the characters "'
" and ""
", and both of these can be escaped with "\
". The character "\
" itself is escaped with "\\
". You can write ""'"
" and "'"'
", but need to escape it if the character is used for delimiting the string: ""\""
" and "'\''
". The characters for line feed ("\n
"), carriage return ("\r
") and horizontal tab ("\t
") also need to be escaped.
The character "\
" is also used to denote line continuation: when placed at the end of a line, it is discarded and the line is joint with the line bellow. Line continuation works inside long strings as well. Commented text is only filtered to the end of the text line (the continuation character). See for example the following valid piece of code:
> mammal(A) :- # a simplified definition \ > blood(A, 'warm'), \ > (hasHair(A); hashFur(A)), \ > # laysEggs(A), Not all of us lay legs! \ > producesMilk(A).
You can also specify ASCII characters in hexadecimal in "\xFF
" notation and Unicode in "\uFFFF
" notation. For Unicode code unit escaping, you can use "\u{FFFFF}
" with an arbitrary number of hexadecimal algarisms. The "\x
" and "\u
" parts are case-sensitive.
Variable names and clause names must start with a letter and be composed of letters, algarisms and underscores.
All whitespace outside of string literals is ignored. This is perfectly legal:
> hello ( World , [ [ [ ' '] ] ]) . # > listing 0: hello(World, [[[' ']]]).
Queries
A query has a similar format to a tailless clause, but ends with a "?
" character instead of ".
". Upon being input, it starts a search for all its solutions using the global table of clauses.
The search will try to find all solutions for which the original query has no outstanding variables, showing the literals that have filled in those variables. When all variables of a clause head are replaced, we say it has unified.
If the original query has no outstanding variables, the query is instead about verifying if that combination of literal values as arguments for the clause unify.
The interpreter will print the values of the variable arguments of every solution found, or return "Yes
" if a solution exists but the query had no variable arguments, or return "No
" if no solution was found.
> parent("john", "anna"). > parent("john", "mary"). > parent("victor", "john"). > parent("sophia", "john"). > parent("victor", "victor jr"). > parent("sophia", "victor jr"). > grandparent(X, Y) :- parent(X, Z), parent(Z, Y). > > grandparent(A, B)? A = 'victor' B = 'anna' A = 'sophia' B = 'anna' A = 'victor' B = 'mary' A = 'sophia' B = 'mary' > grandparent('sophia', 'mary')? Yes > grandparent(A, A)? No
The interpreter will print a warning when a query is impossible to unify when no clauses with the same name and arity were declared.
Short queries
The previous queries that return all the solutions are called full queries. If the clause ends with a "!
" instead of "?
", a short query is performed instead. A short query terminates as soon as the first solution is found. They only return one answer, or "Yes
" and "No
":
> month('January'). > month('February'). > month('March'). > month(name)! month = 'January'
Queries have a time limit to be completed. If a query times out the interpreter prints the message "Search timeout
". A full query can timeout even after finding at least part of the solution. The default time limit is interpreter or system specific.
Previously we said the order of tail clauses is not important, which is true for full queries. With short queries, the first solution found may be different depending on the order of the tail clauses. The interpreter algorithm, however, is stable: given the same definitions the result will be constant (if not introduced by built-in clauses with side effects).
Anonymous arguments
Sometimes we only want to know if a solution exists, or are only interested in part of the solution. Daki allows the use of anonymous arguments in queries, that unify with any value, for this purpose. They are particularly useful when a query would result in an enormous amount of output solutions.
Without anonymous arguments, boilerplate clauses would have to be used instead, that just transform a short query into a long query with discarded arguments. Remember that Daki does not require all arguments in the tail dependencies to unify, contrary to Datalog.
> food('fruit', 'apple', 120). > food('fruit', 'banana', 90). > > # With boilerplate clauses > food(A, B) :- food(A, B, C). > food(A) :- food(A, B). > > food('fruit', 'apple')? Yes > food('fruit', Name)? Name = 'apple' Name = 'banana' > > # With anonymous arguments > food('fruit', 'apple', _)? Yes > food('fruit', Name, _)? Name = 'apple' Name = 'banana' > > # Impossible without anonymous arguments > food(_, _, _)? Yes
Retractions
You can remove a declaraction from the global table of clauses by declaring it again, with a final "~
" instead of ".
". The clause must be equivalent, that is, have all the same facts in the same argument order, except for the variables that may use different names.
If you declare the same rule twice, executing "listing
" will show the number of declarations next to the rule, in parenthesis:
> dinner(X) :- meal(X), time_of_day(18, 23). > dinner(X) :- meal(X), time_of_day(18, 23). > listing 0: dinner(X) :- meal(X), time_of_day(18, 23). (2)
Conversely, to completely retract a rule you must retract it as many times as it is declared.
If you declare a rule that includes logical OR relationships, and is expanded into several rules in memory, you do not need to retract each rule. Since the original statement itself is expanded into many rules, you can retract the whole statement which retracts all generated rules at once:
> star("Sun"). > a(X, Y) :- ((c(Z))); ((b(X); c(X)), (b(Y); c(Y))). > listing 0: star('Sun'). 1: a(X, Y) :- c(Z). 2: a(X, Y) :- b(X), b(Y). 3: a(X, Y) :- b(X), c(Y). 4: a(X, Y) :- c(X), b(Y). 5: a(X, Y) :- c(X), c(Y). > > a(X, Y) :- b(X), b(Y). > a(X, Y) :- c(X), c(Y)~ > listing 0: star('Sun'). 1: a(X, Y) :- c(Z). 2: a(X, Y) :- b(X), b(Y). (2) 3: a(X, Y) :- b(X), c(Y). 4: a(X, Y) :- c(X), b(Y). > > a(X, Y) :- ((c(Z))); ((b(X); c(X)), (b(Y); c(Y)))~ > listing 0: star('Sun'). 1: a(X, Y) :- b(X), b(Y).
To reduce the amount of typying required, you can also retract in-memory rules by their index (the left-most number that appears when executing "listing
". This is done using the built-in command "retract <Index>
".
> a(X, Y) :- ((c(Z))); ((b(X); c(X)), (b(Y); c(Y))). > listing 0: a(X, Y) :- c(Z). 1: a(X, Y) :- b(X), b(Y). 2: a(X, Y) :- b(X), c(Y). 3: a(X, Y) :- c(X), b(Y). 4: a(X, Y) :- c(X), c(Y). > > retract 2 > retract 2 > listing 0: a(X, Y) :- c(Z). 1: a(X, Y) :- b(X), b(Y). 2: a(X, Y) :- c(X), c(Y).
Environment commands
Aside from the language itself, some commands are required to perform operations related to the interpreter and global table themselves. These are:
- quit
- Stop execution and exit the interpreter if in interactive mode; only stop processing the current file if in consult mode
- select_table <Name>
- Changes the global table of clauses currently in use; by default, table "
0
" is active; passing no argument prints the current table number - listing
- Prints all clauses kept in the current global table
- retract <Index>
- Retracts a clause from the current global table specified by index
- consult <File path>
- Read and interpret a Daki language file; receives file path as an argument
- add_memo <Name>
- Add a clause name to the list of clauses to memoize (ex: "
func/3
") - rem_memo <Name>
- Remove a clause name to the list of clauses to memoize; clears the memory pertaining to that clause
- list_memo
- List all clause names of the memoization list
- clear_memo
- Clear all memory spent on clause memoization (does not clear the list of clauses to memoize)
These commands are executed without any trailing ".
", "?
" or "!
", and are case-insensitive. The memo
commands are made clear in the memoization section.
The language features up to here are the minimum required for a pure, logic-based, query-only language. For many real problems, however, this is not enough.
Operator clauses
The Daki language also has built-in clauses, that unify with user-specified clauses and perform some form of calculation. To see why these are important, let's look at a practical example. In a language like Prolog, for instance, calculating a number in the Fibonacci sequence may look like:
fib(1, 1). fib(2, 1). fib(N, X) :- f1 = fib(N - 1, X1), f2 = fib(N - 2, X2), X is X1 + X2.
In Prolog we find arithmetic and conditional logic mixed with the clause itself. In the Daki language, however, we prefer to keep the clause format consistent even for these operations. We use instead what we call operator clauses.
Operator clauses are always unifiable only when the input variables are present, if any, and for performance they are always unified before user-defined clauses where possible. The result of the operation is output in the last argument.
Some operator clauses have variants with the same name but higher arity. This is represented by "...
", bellow and it means that it accepts an infinite number of input arguments.
Arithmetic operator clauses
The inputs must be numeric to unify.
- add(Numeric1, Numeric2, ...., Output).
- Unifies with the result of the addition of the inputs
- sub(Numeric1, Numeric2, Output).
- Unifies with the result of the subtraction of Input1 with Input2
- mul(Numeric1, Numeric2, ..., Output).
- Unifies with the result of the multiplication of the two inputs
- div(Numeric1, Numeric2, Output).
- Unifies with the result of the division of the two inputs; integer division is used if both inputs are integer
- mod(Numeric1, Numeric2, Output).
- Unifies with the rest of the integer division of the two inputs
- pow(Numeric1, Numeric2, Output).
- Unifies with the result ofInput1 to the power of Input2
- sqrt(Numeric, Output).
- Unifies with the result of the square root of Input
- log(Numeric1, Numeric2, Output).
- Unifies with the logarithmic base Input2 of Input1
- round(Numeric1, Numeric2, Output).
- Unifies with the rounded value of Input1 to Input2 decimal cases
- trunc(Numeric, Output).
- Unifies with the value of Input without decimal part
- floor(Numeric, Output).
- Unifies with the largest integer value that is less or equal to the input
- ceil(Numeric, Output).
- Unifies with the smallest integer value that is greater or equal to the input
- abs(Numeric, Output).
- Unifies with the absolute value of the input
- eval(Numeric1, ..., StringN, Output).
- Replaces the instances of a placeholder for each variable "
$N
" in StringN with each numeric value with the same index, and resolves the resulting equation; ignores whitespace; instead of using this clause directly, see inline operations
Bitwise operator clauses
The inputs must be of type Integer to unify.
- bit_and(Integer1, Integer2, Output).
- Unifies with the bitwise AND of the two inputs
- bit_or(Integer1, Integer2, Output).
- Unifies with the bitwise OR of the two inputs
- bit_xor(Integer1, Integer2, Output).
- Unifies with the bitwise XOR of the two inputs
- bit_neg(Integer, Output).
- Unifies with the bitwise inversion of the bits of the input
- bit_shift_left(Integer1, Integer2, Output).
- Unifies with the left shifted value of Integer1 by Integer2
- bit_shift_right(Integer1, Integer2, Output).
- Unifies with the right shifted value of Integer1 by Integer2
Comparison operator clauses
The inputs must be both numeric or both strings to unify.
- eql(Input1, Input2, Output).
- Unifies if the values are equal; with the string literal
'Yes'
- neq(Input1, Input2, Output).
- Unifies if the values are not equal; with the string literal
'Yes'
- max(Input1, Input2, ..., Output).
- Unifies with the maximum value between the inputs; if the inputs are strings, string comparison is used instead of numeric
- min(Input1, Input2, ..., Output).
- Unifies with the minimum value between the inputs; if the inputs are strings, string comparison is used instead of numeric
- gt(Input1, Input2, Output).
- Unifies if Input1 is greater than Input2; if any of the inputs is a string, string comparison is used instead of numeric; unifies with the string literal
'Yes'
- lt(Input1, Input2, Output).
- Unifies if Input1 is lower than Input2; if any of the inputs is a string, string comparison is used instead of numeric; unifies with the string literal
'Yes'
- gte(Input1, Input2, Output).
- Unifies if Input1 is greater or equal to Input2; if any of the inputs is a string, string comparison is used instead of numeric; unifies with the string literal
'Yes'
- lte(Input1, Input2, Output).
- Unifies if Input1 is lower or equal to Input2; if any of the inputs is a string, string comparison is used instead of numeric; unifies with the string literal
'Yes'
Type casting operator clauses
The inputs must be of the correct data type to unify.
- as_string(Input, Output).
- Unifies with the text representation of the input (of any type)
- as_string(Integer1, Integer2, Output).
- Unifies with the text representation of Integer1, with the base specified by Integer2
- as_integer(Input, Output).
- Unifies with the integer value of the input (of any type); will truncate floating point values
- as_integer(String1, Integer2, Output).
- Unifies with the integer value of String1, with the base specified by Integer2
- as_float(Input, Output).
- Unifies with the floating point value of the input (of any type)
String operator clauses
The inputs must be of the correct data type to unify.
- len(String, Output).
- Unifies with the number of characters of a string input
- concat(String1, String2, ..., Output).
- Unifies with the concatenation of the string inputs
- slice(String1, Integer2, Integer3, Output).
- Unifies with the remainder of a string String1 starting at index Integer2 and ending at index Integer3, or the end of the string
- index(String1, String2, Integer3, Output).
- Unifies with the integer position of String2 if found in String1, or integer literal
-1
; searching starting from Integer3 - ord(String, Output).
- Unifies with the numeric Unicode value of the first character in the string input
- char(Integer, Output).
- Unifies with a string with the Unicode character found for the integer input
- split(String1, String2, Output).
- Unifies with a list of the separate string characters of String1, separated by String2; if String2 is the empty string (
''
), every character is separated
List operator clauses
The inputs must be of the correct data type to unify.
- head(List, Output).
- Unifies with the first element of the list; list must not be empty
- tail(List, Output).
- Unifies with the remaining elements of the lest after the first one; will unify with empty list (
[]
) for inputs with a single element, and will not unify with empty lists - push(List1, Input2, Output).
- Unifies with a list with Input2 added at the beginning of List1
- append(List1, Input2, Output).
- Unifies with a list with Input2 added at the end of List1
- put(List1, Input2, Integer3, Output).
- Unifies with a list with Input2 added at position Integer3 of List1; position Integer3 must be valid
- len(List, Output).
- Unifies with the number of elements in the list
- concat(List1, List2, ..., Output).
- Unifies with the concatenation of the list inputs into one
- slice(List1, Integer2, Integer3, Output).
- Unifies with the remainder of list List1 starting at index Integer2 and ending at index Integer3, or the end of the list
- index(List1, Input2, Integer3, Output).
- Unifies with the integer position of Input2 if found in List1, or integer literal
-1
; the search starts from index Integer3 - unique(List, Output).
- Unifies with the the list of unique elements
- reverse(List, Output).
- Unifies with the the list with all elements in the inverse positions
- sort(List, Output).
- Sorts all the elements of the list; all must be of a similar type: numeric, strings or lists
- sum(List, Output).
- Sum all the elements of the list; all must be of a similar type: numeric, strings or lists; the sum of strings will have a similar output to
join/3
, and the sum of lists a similar output toconcat/3
- max(List, Output).
- Find the maximum value of all the elements of the list; all must be of a similar type: numeric, strings or lists
- min(List, Output).
- Find the minimum value of all the elements of the list; all must be of a similar type: numeric, strings or lists
- join(List1, String2, Output).
- Converts each element of the list to string and joins the strings with the String2 separator
- init(Integer1, Input2, Output).
- Unifies with a list of size Integer1 with each element set to Input2
Other operator clauses
These always unify.
- set(Input, Output).
- Just transposes the input to the ouput variable
- rand(Output).
- Unifies with a random floating point value between 0 and 1
- type(Input, Output).
- Unifies with the string literal of the name of the data type of Input:
'string'
,'integer'
or'float'
- print(Input, Output).
- Print Input to the console; unifies with the string literal
'Yes'
- print(Input1, Input2, Output).
- Print Inpu1 to the console; Input2 is used to enforce order of execution; unifies with the string literal
'Yes'
- time(Output).
- Unifies with the integer number of milliseconds since the UNIX epoch
- time(Input, Output).
- Unifies with the integer number of milliseconds since the UNIX epoch; the input is used to enforce order of execution
Operator clauses cannot be overwritten or retracted with clauses with the same name and arity. They also only unify with some data types - for instance an arithmetic clause will not unify with string arguments. Illegal argument combinations, like trying to divide by 0, also do not unify.
Let's now go back to how to implement a program that returns the value of the Fibonacci sequence at position N. At first glance, the solution in Daki would be:
> fib(1, 1). > fib(2, 1). > fib(N, Res) :- gt(N, 2, Y), sub(N, 1, N1), sub(N, 2, N2), fib(N1, F1), fib(N2, F2), \ add(F1, F2, Res).
Since this solution is recursive: a dependency on fib
will try all solutions by expanding all clauses named fib
, including itself; does this result in an infinite cycle? Not quite. A Daki interpreter is smart enough to stop the whole tail calculation when one of it's parts is proven impossible to unify, and most cases we don't have to think hard about this: operator clauses are guaranteed to have higher priority to being processed, and thus the expansion of the clause tail can be aborted sooner.
Clause conditions
The Daki interpreter, however, is not all-knowing. A more complex example may require many clause expansions to reach a falsifiable part. The best solution would be to avoid expanding clauses dependencies as early as possible.
This is best achieved by using what we call clause conditions. Clause conditions are boolean tests on variables of the head of candidate clauses to be evaluated. The condition is tested at the clause selection phase, which is more efficient. With clause conditions our Fibonacci program becomes:
> fib(1, 1). > fib(2, 1). > fib(N > 2, Res) :- sub(N, 1, N1), sub(N, 2, N2), fib(N1, X1), fib(N2, X2), \ add(X1, X2, Res).
The clause condition fib(N > 2, Res)
restricts matching N to values greater than the numeric value 2. The full list of operators is as follows.
Condition Operators
- <
- Tests if the variable is lower than the literal; not available for
list
types - <=
- Tests if the variable is lower or equal to the literal; not available for
list
types - >
- Tests if the variable is greater than the literal; not available for
list
types - >=
- Tests if the variable is greater or equal to the literal; not available for
list
types - <>
- Tests if the variable is not equal to the literal
- :
- Tests if the data type of the variable is the literal value (from
'integer'
,'float'
,'string'
or'list'
)
Clause conditions are exclusively between a variable and a literal value, and different data types never unify, except if they are both numeric. This contrasts with the usual clause matching rules that do not unify integer with floating point literals. The comparison operators use alphabetical order for strings.
Also note that you can mix multiple conditions. A variable must match all conditions for the clause to be expanded:
> positive_except_five1(0 < N, N <> 5.0). > positive_except_five(N) :- positive_except_five1(N, N). > > positive_except_five(3)? Yes > positive_except_five(4.50)? Yes > positive_except_five(5)? No > positive_except_five(-3)? No > positive_except_five('1')? No > is_string(X: 'string'). > is_string(1)? No > is_string(1.0)? No > is_string("1")? Yes > is_float(X: 'float'). > is_float(1)? No > is_float(1.0)? Yes > is_float("1")? No > is_integer(X: 'integer'). > is_integer(1)? Yes > is_integer(1.0)? No > is_integer("1")? No > is_numeric(X) :- is_float(X); is_integer(X). > is_numeric(1)? Yes > is_numeric(1.0)? Yes > is_numeric("1")? No
As a last example, we can also benchmark how fast our two Fibonacci functions are, by making use of the time/2
and time/3
operator clauses:
> # Using only operator clauses > fib1(1, 1). > fib1(2, 1). > fib1(N, Res) :- gt(N, 2, gt), sub(N, 1, N1), sub(N, 2, N2), fib1(N1, X1), \ fib1(N2, X2), add(X1, X2, Res). > time_fib1(N, Val, Elapsed) :- time(StartTime), fib1(N, Val), time(Val, EndTime), \ sub(EndTime, StartTime, Elapsed). > > time_fib1(10, Val, Elapsed)? Val = 144 Elapsed = 161 > # Using a clause condition > fib2(1, 1). > fib2(2, 1). > fib2(N > 2, Res) :- sub(N, 1, N1), sub(N, 2, N2), fib2(N1, X1), fib2(N2, X2), \ add(X1, X2, Res). > time_fib2(N, Val, Elapsed) :- time(StartTime), fib2(N, Val), time(Val, EndTime), \ sub(EndTime, StartTime, Elapsed). > > time_fib2(10, Val, Elapsed)? Val = 144 Elapsed = 99
As you can see, using only operator clauses where a clause condition could've been used can result in a large performance penalty. Operator clauses are obviously still useful for intermediate calculations, but should be avoided for logic control.
Inline Operations
Even though operator clauses are very powerful, it can still be somewhat cumbersome to write mathematical formulas using them. For this reasion, some of the more common numeric operations can still be used inline, as such:> is_power(N, N2) :- eql(N * N, N2, Res). > is_power(3, 9)? Yes > print_more(N, L) :- print(N + (L * 2), Out1), \ print(N * 2, Out2), \ print(N * N, Out3), \ print((N + 1) * N + 1, Out4). > print_more(5, 2)? 9 10 25 31 Yes
Inline operations are limited to the following numeric operations and the use of parenthesis. Invalid operations like dividing by zero do not unify.
Inline Operators
- +
- Numeric addition
- -
- Numeric subtraction
- *
- Numeric multiplication
- /
- Numeric division; integer division if both operands are integer
- %
- Remainder of the numeric division
- &
- Bitwise AND
- |
- Bitwise OR
- ^
- Bitwise XOR
- ~
- Bitwise complement
Notice the bitwise complement operator is the only one with a single operand. It is used as, for example, "~123
", so you will need to surround it with parenthesis if in the middle of a longer expression.
In practice, inline operations are expanded into using the eval
operator clause with an intermediate variable for the output. You can see this seamless expansion by using the command listing
.
> sum_of_powers(X, Y, Res) :- set((X * X) + (Y * Y), Res). > sum_of_powers(3, 2, Sum)? Sum = 13 > listing 0: sum_of_powers(X, Y, Res) :- eval(X, Y, '($0*$0)+($1*$1)', $1), set($1, Res).
Inline operations, the eval
operator clause and in fact the entire parsing algorithm uses deterministic finite-state machines for the evaluation. Because of this the operators do not have an intrinsic operator priority, they are evaluated sequentially. You must use parenthesis to enforce an evaluation order.
Memoization
In the last example where we calculated the value of position N of the Fibonacci sequence, we were recalculating many intermediate values. In some contexts, this is required, because a clause can be expanded in many ways; other types this can be avoided, and we can apply memoization to the known unifiable forms of the clause.
In the Daki language this is done by telling the interpreter what functions can be memoized:
> # Having fib1, fib2, time_fib1 and time_fib2 defined before > > add_memo fib1/2 OK > add_memo fib2/2 OK > > time_fib1(12, Val, Elapsed)? Val = 144 Elapsed = 11 > time_fib2(12, Val, Elapsed)? Val = 144 Elapsed = 8 > > # Values have already been completely memoized: > > time_fib1(12, Val, Elapsed)? Val = 144 Elapsed = 0 > time_fib2(12, Val, Elapsed)? Val = 144 Elapsed = 0
In this example, we have instructed the interpreter to remember the argument configurations for which our functions unify, and, where possible, to reuse those configurations instead of starting new searches. We must be very careful to be consistent in how we expand our requirements on a memoized clause. If we later tried to find out all positions in the Fibonacci sequence for which the value is 1, it would fail to give all the solutions, because it has already cached one configuration that unifies:
> fib2(X, 1)? X = 1
Memoization is always relative to a global clauses table. Changing to another table will use another memoization tree. Adding or retracting individual clauses does not affect what clauses are memoized, nor is the process memory cleared, use the built-in commands rem_memo
and clear_memo
for that.
As a last example, let's create a small program that prints the first twenty elements of the Fibonnaci sequence, using memoization, lists and inline operations:
> add_memo fib/2 OK > > fib(1, 1). > fib(2, 1). > fib(N > 2, Res) :- fib(N - 1, X1), fib(N - 2, X2), add(X1, X2, Res). > > fib_seq(0, []). > fib_seq(N > 0, Out) :- fib(N, Val), init(1, Val, Out2), fib_seq(N - 1, Out1), \ concat(Out1, Out2, Out). > > time_fib2(N, Out, Elapsed) :- time(StartTime), \ fib_seq(N, Out), \ time(Out, EndTime), \ sub(EndTime, StartTime, Elapsed). > > print_fib(N) :- time_fib2(N, Out, Elapsed), \ join(Out, ", ", Seq), \ concat("Sequence: ", Seq, Line1), \ print(Line1, ok), \ as_string(Elapsed, ElapsedStr), \ concat("Elapsed (ms): ", ElapsedStr, Line2), \ print(Line2, ok). > > print_fib(20)? Sequence: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 41 81, 6765 Elapsed (ms): 95 Yes
Copyright (c) 2021 Gonçalo Mendes Ferreira
Permission to use, copy, modify, and/or distribute this document for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.