Relation generators can take the place of functions, to split programs into smaller, re-usable components.
Some built-in routines are implemented this way.
For example, the maths
module has a relation named square_root
which uses code to generate its values but, to the outside world, behaves like a normal relation.
import("maths")
print(maths.square_root(s:=25.0))
┌───────┐ │ r │ ├───────┤ │ -5.00 │ │ 5.00 │ └───────┘
Some differences from most programming languages to notice:
First, this square root function returns both of the answers: i.e. -5 and +5.
Second, this square root function can accept multiple arguments at once. This:
- Avoids caller loops, both syntax-wise and call overhead-wise, both in passing arguments and unpacking results. The user can apply the function to a whole set of data at once (since the call is effectively a join)
- Gives the calculation engine the opportunity to use parallel/GPU methods to calculate the answers
Third, this square root function can work in both directions: if we pass it a root it can return the square.
This is why it's named square_root
and not sqrt
, as a reminder of the two parts (with the mnemonic parameter names s
and r
).
import("maths")
print(maths.square_root(r:=-5.0))
┌───────┐ │ s │ ├───────┤ │ 25.00 │ └───────┘
This might not be so useful in some cases but for other functions this can greatly reduce the number of variations of them that we need.
For example, the str_rel
function can be used to split a string into a relation, with a character per tuple by default.
print(str_rel(s:="hello world"))
┌─────┬──────────────┐ │ sep │ r │ ├─────┼──────────────┤ │ │ ┌─────┬────┐ │ │ │ │ seq │ ss │ │ │ │ ├─────┼────┤ │ │ │ │ 0 │ h │ │ │ │ │ 1 │ e │ │ │ │ │ 2 │ l │ │ │ │ │ 3 │ l │ │ │ │ │ 4 │ o │ │ │ │ │ 5 │ │ │ │ │ │ 6 │ w │ │ │ │ │ 7 │ o │ │ │ │ │ 8 │ r │ │ │ │ │ 9 │ l │ │ │ │ │ 10 │ d │ │ │ │ └─────┴────┘ │ └─────┴──────────────┘
And it can also be used to convert a relation of characters into a string.
print(str_rel(r:={seq:int,ss:str}{[0,"h"],[1,"e"],[2,"l"],[3,"l"],[4,"o"],
[5," "],[6,"w"],[7,"o"],[8,"r"],[9,"l"],[10,"d"],}))
┌─────────────┬─────┐ │ s │ sep │ ├─────────────┼─────┤ │ hello world │ │ └─────────────┴─────┘
If fact, the same function can also be used to split a string into words or to join many words into one string (by passing an argument for sep
, e.g. " ").
print(str_rel(r:={seq:int,ss:str}{[0,"hello"],[1,"world"],}, sep:=" "))
┌─────────────┐ │ s │ ├─────────────┤ │ hello world │ └─────────────┘
This way, a single function can do the work of four or more, and can be called once (many ways depending on which arguments it receives) for a set of data instead of repeatedly for each instance.
The three differences listed above allow us to compose relations to re-shape and transform data en-bloc, reducing the need for code. Less code = fewer bugs, and leads to systems that are easier to reason about and change.
In the examples above, we were using the compose and relation-creating short-hand to effectively join a relation containing our single argument with the maths.square_root
relation.
If we use the longer version we can pass multiple tuples at once, i.e. a relation:
import("maths")
print(maths.square_root & {s:float}{[25.0], [49.0]})
┌───────┬───────┐ │ s │ r │ ├───────┼───────┤ │ 25.00 │ -5.00 │ │ 25.00 │ 5.00 │ │ 49.00 │ -7.00 │ │ 49.00 │ 7.00 │ └───────┴───────┘
Instead of joining, we could use compose and then the results wouldn't include the arguments we pass. This is often what's needed when using such functions e.g.
import("maths")
maths.square_root(S{*, r:=to_float(STATUS)}){*, STATUS_SQUARED:=s}
SNO | SNAME | STATUS | CITY | STATUS_SQUARED |
---|---|---|---|---|
S1 | Smith | 20 | London | 400 |
S2 | Jones | 10 | Paris | 100 |
S3 | Blake | 30 | Paris | 900 |
S4 | Clark | 20 | London | 400 |
S5 | Adams | 30 | Athens | 900 |