We can define new relation operations in addition to the built-in ones. A relational operator takes zero or more relations and returns a relation.
We use op
to declare a relation operator followed by the parameters in parentheses, and then the output type, and then a block of code between a begin
and end
.
Inside the block of code, there should be a return
statement to pass the result back to the caller.
The following example takes a relation with a heading of type {x:str, y:str}
and returns the transitive closure with the same heading type. We give the input parameter a name of r
.
tclose := op(r:{x:str, y:str}) {x:str, y:str} begin
ttt:=r | ((r(r{z:=y, y:=x})){*, y:=z})
if ttt = r begin
return ttt
end else begin
return tclose(ttt)
end
end
We call the operator using parentheses, e.g.
mm:={MAJOR_P:str, MINOR_P:str}{
["P1", "P2"]
["P1", "P3"]
["P2", "P3"]
["P2", "P4"]
["P3", "P5"]
["P4", "P6"]
}
print(tclose(mm{x:=MAJOR_P, y:=MINOR_P}))
┌────┬────┐ │ x │ y │ ├────┼────┤ │ P1 │ P2 │ │ P1 │ P3 │ │ P1 │ P4 │ │ P1 │ P5 │ │ P1 │ P6 │ │ P2 │ P3 │ │ P2 │ P4 │ │ P2 │ P5 │ │ P2 │ P6 │ │ P3 │ P5 │ │ P4 │ P6 │ └────┴────┘
Wildcard parameters, *
Instead of having to specify the complete heading for each parameter, *
can be used to stand for 'any other attributes, of any type'.
For example, s:{*}
would mean a relation parameter, named s
within the operator code block, with any number of attributes.
r:{*, x:int}
would mean a relation parameter, named r
within the operator code block, with an attribute named x
of type int
and zero or more other attributes.
As well, *
can stand for 'any type'. So r:{x:*, y:*}
would mean a relation parameter, named r
within the operator code block, with two attributes named x
and y
of any type.
attrs()
Given a variable number of possibly unnamed attributes, possibly with types not known until call-time, the output type may need to dynamically adjust based on the headings of the arguments at call-time.
To support this, we can use the attrs
function to build a set of attribute declarations and return them in a format suitable for use in other operations, such as the output type of a relation operator or a relation projection.
The attrs
function is special here in that it is evaluated each time the operator is called, not when it is declared.
As an example, suppose we want to add a compose
operator (in addition to the built-in method of nesting relations using parentheses). It would need to handle relations with any number of attributes and return a relation based on the types of inputs given.
We can use the heading
function to get a set of attributes for a relation parameter and so build up a suitable set of attributes to pass to the attrs
function.
compose := op(r:{*}, s:{*}) {attrs((heading(r)|heading(s)) - (heading(r)&heading(s)))} begin
return (r&s){attrs((heading(r)|heading(s)) - (heading(r)&heading(s)))}
end
The operator above declares 2 relation parameters, r and s, with any headings at all. It declares the output to be a relation with attributes from the union of all the attributes from r and s, whatever they may be at time of calling, with the intersection of the attributes of r and s removed (i.e. remove the ones they have in common). The return statement uses the same construction to project the required attributes to form the result.
Calling this with two sample relations, in this case with one common attribute:
print(compose(
{c:float, b:str}{[3.0,"b"],[5.0,"b"]},
{c:float, d:str}{[2.0,"d"],[5.0,"d"],[3.0,"e"]}
))
┌───┬───┐ │ b │ d │ ├───┼───┤ │ b │ e │ │ b │ d │ └───┴───┘