Annotations
A definition can be preceded by a number of optional annotations.
An annotation consists of the character @
, immediately followed by an identifier: for example, @inline
.
By convention, the annotations of a definition are written on a separate line.
This section briefly describes the most commonly used annotations, which are listed in lexicographic order below. The list is not complete and is likely to change over time. You may want to bookmark this page for future reference. Most of the omitted annotations have very specialized purposes, such as displaying the intermediate results of compilation or suppressing a particular optimization.
Annotation | Explanation |
---|---|
@function | Ensures that the relation is a function. Addition of a tuple that has the same keys as an existing one causes the existing one to be deleted. |
@inline | Expands the definition wherever it is used. Can be used for parameterized modules and for nonrecursive higher-order definitions, but in those cases @outline is usually more suitable. |
@ondemand | Prevents the relation from being fully materialized. Rel will compute only those parts that are actually used. Useful especially for infinite relations. |
@outline | Used for definitions that feature higher-order variables, that is, for most parameterized modules and for higher-order definitions, even recursive ones. |
@static | Used for integrity constraints that can be evaluated statically, that is, from the schema, rather than from actual data. |
Here is a quick overview of three annotations that are related closely enough to sometimes cause confusion:
@ondemand
does not, by itself, allow you to use higher-order variables but is used to annotate infinite relations that are used “by need.” You may have to combine it with@outline
if the infinite relation uses higher-order variables.@inline
and@outline
do allow higher-order variables.@inline
does not allow recursive definitions, whereas@outline
does.@outline
is generally preferable to@inline
. You may have to combine them with@ondemand
if the relation is infinite.
See also Combining Annotations.
Although an annotation is a part of a definition, in most cases it annotates an entire relation. That is, if there are several definitions for a particular combination of relation name, arity, and type, then it is enough to annotate only one of these definitions. It is usual to annotate the first definition, but this is not a requirement.
A notable exception to this rule is the annotation @outline
, which is associated with a particular definition.
See the end of section @outline
for more details.
@function
The annotation @function
is not intended for general use.
The main purpose of annotating some relation R with @function
is to indicate that R is a function.
In some situations this allows the system to perform optimizations that may drastically decrease the computational cost of your application.
It’s your responsibility to ensure that R really is a function: the annotation will not always do that for you.
If R turns out not to be a function, then the optimized logic may yield incorrect results.
A relation is a function if there is only one value for a given tuple of keys. By default, the value is the last element of a tuple and the keys are all the other elements.
In the example below, F
is a function, but G
is not:
def F { 1, 10, 101; 2, 10, 200 }
def F { 1, 20, 100 }
def G { 1, 10, 101; 2, 10, 200 }
def G { 1, 10, 100 }
In many cases Rel does attempt to automatically make the annotated relation a function.
For instance, if you annotate the definition of G
in the example above, only one tuple with the keys 1
and 10
will be retained:
// read query
@function
def G { 1, 10, 101; 2, 10, 200 }
def G { 1, 10, 100 }
def output = G
If a derived relation has several definitions, it is enough to annotate one of them, not necessarily the first.
This is true for all annotations, except @outline
.
However, if the defined name refers to several different relations — if there are several definitions of the name with different types or arities — then the annotation must be repeated at least once for each relation that you want to declare as a function.
You may use @function
to annotate the first insertion into a base relation.
When you later add a new tuple to the base relation, the addition is often preceded by the automatic deletion of an existing tuple that has the same keys and a different value.
This is illustrated in the following example:
// write query
@function
def insert[:BaseF] = 1, 1 ; 2, 2 ; 1, 0 ; 3, 3
// read query
def output = BaseF
// write query
def insert[:BaseF] = 1, 2; 2, 1
// read query
def output = BaseF
For derived relations, there is no sequence of insertions and/or deletions. A derived relation is always considered a whole, even if it is defined piecemeal in different transactions. For instance:
// model
@function
def DerF = 1, 1 ; 2, 2 ; 3, 3
// read query
def output = DerF
// model
def DerF = 1, 2; 2, 1
// read query
def output = DerF
If there is more than one tuple with the same key, the lexicographically smaller one is kept in the relation.
Note that @function
is an experimental feature.
You can’t always rely on it to enforce functional dependence within a relation.
@inline
When a definition of relation R
is annotated with @inline
, the annotation makes Rel treat the definition differently.
The relation R
is not materialized; instead, its definition is inlined, or expanded at every place where it is used.
More precisely, a copy of the body of the definition of R
replaces every relational application of R
, after replacing the parameters with the appropriate arguments.
Consider the following example:
@inline
def P[x, y] = x * x + y
def Q[x] = P[x, x] + P[2.0, 3.0]
The annotation makes the definition of Q
equivalent to:
def Q[x] = x * x + x + 2.0 * 2.0 + 3.0
An inlined definition — or a series of such definitions — cannot be recursive. Such direct or indirect recursion would lead to infinite expansion, so it is treated as an error.
The following example features indirect recursion: P
uses Q
, and Q
uses P
.
// read query
@ondemand
def P[x in Int] = Q[x] * 2
@ondemand
def Q[x in Int] = if x > 1 then P[x - 1] else 1 end
def output = P[4]
If you change the annotations on either P
or Q
to @inline
, the result will be the same.
But if you add @inline
to both the definitions, the compiler will raise an error.
An inlined definition can have occurrences of variables that are not grounded, as long as they are grounded in the definitions in which it is expanded. One consequence of this is that an inlined definition that appeared to be correct may suddenly cause an error when used in a new way.
For instance, in the example above, you can replace either one of the occurrences of @ondemand
with @inline
.
If the definition of P
and/or Q
is deprived of annotations, x
will be ungrounded and Rel will raise an error.
You can use the annotation @inline
to annotate nonrecursive higher-order definitions, that is, definitions of relations whose parameters are other relations.
In general, however, it is preferable to use @outline for this purpose.
See Higher-Order Definitions for more details.
You can also use the annotation @inline
in the definitions of parameterized modules.
It is best to use @outline
there, as well.
The annotation @inline
causes the definition of a relation R
to be treated, roughly speaking, as a C macro.
That is, the definition of R
is expanded wherever R
is used.
This is why it cannot be used for recursive definitions.
Consider the following example, which is yet another version of transitive closure:
@inline def tc[E] = E; E . (tc[E])
def edges = (1, 2); (2, 3); (3, 4)
def output = tc[edges]
The compiler rewrites this as:
def output = edges; edges . (tc[edges])
This, in turn, is rewritten as:
def output = edges; edges . (edges; edges . (tc[edges])))
There is no end to such rewriting, so the compiler reports an error.
Compare this to the same example described in section @outline
.
Automatic Inlining
Some definitions are inlined by the compiler even if they are not annotated with @inline
.
This happens, in particular, when:
- The compiler can determine that the arguments of a relation are ungrounded.
- The definition of that relation is not recursive.
- The types of variables are given or can be inferred.
For instance, consider the following definition of an infinite relation f
:
def f[x in Int] = x * x + x
The partial relational application f[10]
will be replaced with the expression 10 * 10 + 10
, whose value is 110
.
See also the example at the end of Grounded Variables.
When in doubt about whether a definition will be automatically inlined, you can always annotate it with @inline
.
@ondemand
This annotation prevents Rel from materializing the defined relation. Rel computes only those parts of the relation that are actually used. This is particularly useful if the relation is infinite.
The compiler achieves this by performing a demand transformation, which will be explained in this section.
In simple cases there is no need to add the annotation, as the compiler can infer that certain infinite relations must be subjected to a demand transformation.
Using @ondemand
in those cases does no harm.
The example below implements the Library relation range
in Rel.
The relation my_range
is infinite, so, if the annotation is missing, Rel produces an error message informing you that low
, high
, and stride
are ungrounded.
The annotation allows my_range
to be materialized to just the subset that is relevant for the given arguments:
// read query
@ondemand
def my_range[low in Int, high in Int, stride in Int] = low, low <= high
def my_range[low in Int, high in Int, stride in Int] =
my_range[low + stride, high, stride], low < high
def output = my_range[1, 6, 2]
Notice that in this case you cannot replace @ondemand
with @inline
, because the definition is recursive.
By annotating the definition of some relation R with @ondemand
you inform the compiler that it should perform a demand transformation, that is, that it should rewrite the definition in such a way that R becomes finite.
The rewriting takes into account the ways in which R is used.
Consider the following example, in which the @ondemand
annotation is omitted because the compiler infers it for sufficiently simple relations:
def f[x in Int] = 2*x // Infinite relation
def output = f[3] + f[4]
The compiler rewrites it to something similar to the following:
def f[x in Int] = 2 * x, demand_of_f(x)
def demand_of_f(x) { x = 3 or x = 4 }
def output = f[3] + f[4]
The relation demand_of_f
is finite, so the addition of the conjunct demand_of_f
to the body of the definition of f
causes f
to become finite.
If the infinite relation is recursive, the rewriting must be a little more involved. The following is a definition of the factorial function:
@ondemand
def f[0] = 1
def f[n] = n * f[n - 1], n > 0
def output = f[5]
The compiler rewrites this more or less as follows:
// read query
def f[0] = 1, demand_of_f_1(0)
def f[n] = n * f[n - 1], n > 0, demand_of_f_1(n)
def demand_of_f_1(m) { m = n - 1, n > 0, demand_of_f_1(n) from n }
def demand_of_f_1 = 5
def output:aux = demand_of_f_1
def output:all = f
The relation demand_of_f_1
is recursively defined, but it is finite.
It serves to make the rewritten f
finite, as well.
@outline
The annotation @outline
supports higher-order definitions.
A higher-order definition is a definition of a relation, some of whose parameters represent non-singleton relations.
See Higher-Order Definitions for more details.
Specifically, @outline
directs the compiler to use the annotated definition as a template from which to generate a first-order definition that is specialized on the higher-order parameters. For example, R
is the higher-order parameter in the following definitions of a transitive closure relation:
// read query
// Compute the transitive closure of a binary relation.
@outline
def transitive_closure[R] = R
@outline
def transitive_closure[R] = R . (transitive_closure[R])
def edges = (1, 2); (2, 3); (3, 4)
def output = transitive_closure[edges]
The compiler rewrites this to a first-order form similar to the following:
// read query
def transitive_closure_edges = edges
def transitive_closure_edges = edges . (transitive_closure_edges)
def edges = (1, 2); (2, 3); (3, 4)
def output = transitive_closure_edges
This example features the composition operator.
Notice the parentheses in the second higher-order definition above.
If you omit them, the definition will be syntactically incorrect.
A different version of transitive_closure
is shown in Higher-Order Definitions.
The annotation @outline
is necessary in the second definition above, because the definition is recursive.
If a higher-order definition is not directly or indirectly recursive, you can replace the annotation @outline
with @inline
.
One of the two annotations must be used for any higher-order definition.
If a higher-order definition is not recursive, then @inline
is sometimes preferable to @outline
, because the former can help avoid materialization of a large relation.
An example of this phenomenon is discussed in Combining Annotations.
All else being equal, it is best to use @outline
rather than @inline
for higher-order definitions.
This also applies to annotations of parameterized modules.
If there are multiple definitions for a given relation, then @outline
must annotate each of those definitions that are recursive and feature a higher-order variable, unless such definitions are in a module that has been annotated with @outline
.
@static
You can use this annotation for integrity constraints that can be evaluated statically, that is, on the basis of the schema, without looking at the data.
If the compiler cannot evaluate an integrity constraint statically, it will ignore the annotation.
See the @static
annotation in the Integrity Constraints concept guide.
Combining Annotations
The compiler allows the three annotations, @inline
, @ondemand
, and @outline
to be combined.
However, it makes sense to combine only two of these: @outline
and @ondemand
.
The order in which you write them does not matter.
The annotation @outline
always takes effect before @ondemand
.
Combining @outline
with @ondemand
, even when not necessary, can affect execution time.
Here is a simple example:
@outline
def my_add(R, x, y, z) {
R(x) and R(y)
and z = x + y
}
def output = my_add[range[1, 10000, 1], 3, 4]
This can take a noticeable time to compute, because the relation that is the first argument of my_add
has ten thousand elements and is materialized.
After adding @ondemand
, the relation my_add
will be rewritten in a way that will prevent this materialization from occurring, so the computation will become noticeably faster:
@outline @ondemand
def my_add(R, x, y, z) {
R(x) and R(y)
and z = x + y
}
def output = my_add[range[1, 10000, 1], 3, 4]
In this case you can achieve a very similar speedup by replacing @outline
with @inline
.
In general, it is difficult to formulate hard-and-fast rules for such tweaks, and you will not always need them. Nonetheless, a little experimentation can help improve your model in the shorter term.
Next: Specialization