We can define new operations on relations in addition to the built-in ones. A relation 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 for internal use within the operator code (including possible use in the output type if using attrs or attr_types - see below). Operator parameter names are not used when calling the operator - only the parameter positions are used.
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() and attr_types()
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 attr_types 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 heading declaration.
The attr_types 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 attr_types function.
compose := op(r:{*}, s:{*}) {attr_types((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 but uses attrs instead of attr_types to project the required attributes (so no types needed) 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 │ └───┴───┘