Advanced

Relation operators

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 │
└───┴───┘