Advanced

More relation generators

Relation generators can act as functions or subroutines, 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 top which stands for Triangle of Power, to handle roots, exponents and logs symmetrically.

a b c

This uses code to generate its values but, to the outside world, behaves like a normal relation.

import("maths")
print(maths.top(b:=2.0, c:=25.0))  // square root of 25.0
┌───────┐
│ a     │
├───────┤
│ -5.00 │
│  5.00 │
└───────┘

Some differences from most programming languages to notice:

First, this relation generator returns both of the answers for a square root: i.e. -5 and +5.

Second, this relation generator can accept multiple sets of arguments at once. This:

  1. Avoids caller loops, both syntax-wise and call overhead-wise, both in passing arguments and unpacking results. The user can apply the relation generator to a whole set of data at once (since the call is effectively a join)
  2. Gives the calculation engine the opportunity to use parallel/GPU methods to calculate the answers

Third, this relation generator can work in many directions: if we pass it a root it can return the square:

import("maths")
print(maths.top(a:=-5.0, b:=2.0))  // square of -5.0
┌───────┐
│ c     │
├───────┤
│ 25.00 │
└───────┘

And if we pass it a base and a power it can return the log:

import("maths")
print(maths.top(a:=5.0, c:=25.0))  // log base 5.0 of 25.0
┌──────┐
│ b    │
├──────┤
│ 2.00 │
└──────┘

This greatly reduces the number of different functions that we need.

str_rel

For example, the str_rel relation generator can be used to split a string s into a relation r{seq:int, ss:str}, 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 relation generator 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 relation generator can do the work of four or more functions, and can be called once (many ways depending on which arguments it receives) for a set of data instead of repeatedly for each instance.

We can further manipulate the results with Ra expressions, drastically reducing the number of functions needed.

For example we can use str_rel to get the first 5 characters in a string, reverse them and built a new string with '-' between each letter:

print(str_rel(r:=str_rel(s:="hello world")..r[seq<5]{*, seq:=-seq}, sep:="-"))
┌───────────┐
│ s         │
├───────────┤
│ o-l-l-e-h │
└───────────┘

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.

Bulk operations

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.top relation. If we don't compose with a single set of arguments, and instead use an explicit join, we can pass multiple tuples at once, i.e. a relation:

import("maths")
print(maths.top & {c:float, b:float}{[25.0, 2.0], [49.0, 2.0], [125.0, 3.0]})
┌────────┬──────┬───────┐
│ c      │ b    │ a     │
├────────┼──────┼───────┤
│  25.00 │ 2.00 │  5.00 │
│  25.00 │ 2.00 │ -5.00 │
│  49.00 │ 2.00 │  7.00 │
│  49.00 │ 2.00 │ -7.00 │
│ 125.00 │ 3.00 │  5.00 │
│ 125.00 │ 3.00 │ -5.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 relation generators e.g.

import("maths")
maths.top(S{*, a:=to_float(STATUS), b:=2.0}){*, STATUS_SQUARED:=c}
SNOSNAMESTATUSCITYSTATUS_SQUARED
S1Smith20London400
S2Jones10Paris100
S3Blake30Paris900
S4Clark20London400
S5Adams30Athens900