Skip to content
Rel
REL CONCEPTS
Value Types

Value Types

This guide describes a feature of Rel that allows you to define your own types.

Introduction

There are two kinds of types that you can define yourself: entity types and value types. A value type has its own name and is distinct from every other type, even though its values are represented by tuples of primitive types. This allows you to make your model more clear and readable. It also helps you to avoid some common mistakes.

A Simple Example

You have built the first small fragment of a toy railway, and you intend to expand it significantly. You decide to build a computer model of the rail network, so that you can answer questions about transit times, distances, and the like. This will come in handy when the network grows.

There are three stations: “A”, “B”, and “C”. You know the distances between them, and how much time it takes to travel from one to the other:

// install

def distance = (:A, :B, 100.0)
def distance = (:B, :C,  50.0)

def travel_time = (:A, :B, 30)
def travel_time = (:B, :C, 20)

This captures your data, but the model has a problem. Since the distances and time intervals are represented by numbers, it is all too easy to get confused and perform operations that should not be allowed. For instance, you can add a value that represents a distance to a value that represents a time interval:

// query

def output[x, y] = distance[x, y] + travel_time[x, y]
Loading q-raw...

This doesn’t make much sense, and you would like to be protected from such mistakes. In other words, you would like a number that represents a distance to be clearly distinguishable from a number that represents a time interval. Rel allows you to do that by declaring value types.

Declaring Simple Value Types

The declaration

value type Distance = Float

introduces a new type, Distance, whose values are represented by floating-point numbers but are distinct from values of all other types.

The declaration also introduces an infinite binary relation, ^Distance, which is often referred to as the constructor of Distance. The relation ^Distance is a one-to-one map between floating-point numbers and the corresponding values of type Distance. It can be used both to construct value types (^Distance[100]) and to retrieve the actual value(s) from it (x: ^Distance(x, ^Distance[100])).

Note that there is nothing special about identifiers that begin with the character ^, but in the interest of readability it is best to avoid using them for other purposes.

Starting from scratch, you rewrite the model:

// install

value type Distance = Float
value type Duration = Int

def distance = (:A, :B, ^Distance[100.0])
def distance = (:B, :C, ^Distance[50.0])

def travel_time = (:A, :B, ^Duration[30])
def travel_time = (:B, :C, ^Duration[20])

The square brackets are important! The partial application ^Distance[100.0] evaluates to the value of type Distance that corresponds to the number 100.0. The first definition of distance above could also have been written as def distance = (:A, :B, {d : ^Distance(100.0, d)}).

It may be instructive to display the relation distance:

// query

def output = distance
Loading q-distance...

The heading of the third column indicates that the column shows floating-point representations of the type Distance, which is shown as the symbol :Distance.

For good measure, here is the relevant part of the implicit relation ^Distance.

// query

def output(x, y) = ^Distance(x, y) and distance(_, _, y)
Loading q-conversion...

The problematic addition of distances to time intervals no longer works. If you try it, the output will be empty, just as it would be for def output = 5 + "five":

// query

def output[x, y] = distance[x, y] + travel_time[x, y]
Loading q-bad-sum...

However, not all is well yet. It would be nice to add a distance to another distance, but addition is not defined for values of type Distance:

// query

def output = distance[:A, :B] + distance[:B, :C]
Loading value-cannot-add...

Currently such operations on value types must be defined by the user, as shown in the following subsection. Automatic support will be provided in the future.

Extracting the Representation of a Value (Simple Case)

To define addition for two values of type Distance, you must extract the associated floating-point numbers from ^Distance. This can be done as follows:

// query

def (+)[x in Distance, y in Distance] =
    xf + yf from xf, yf where ^Distance(xf, x) and ^Distance(yf, y)
def output = distance[:A, :B] + distance[:B, :C]
Loading q-add-distances1...

The above is a little clunky, because it uses the variables xf and yf rather than a partial application of the relation ^Distance. The latter could not be used, because the desired numeric value is the first element of each tuple, not the second one. This may be remedied by using the library function transpose, which reverses the order of elements in a binary relation:

// install

def (+)[x in Distance, y in Distance] =
    transpose[^Distance][x] + transpose[^Distance][y]
// query

def output = distance[:A, :B] + distance[:B, :C]
Loading q-add-distances...

Everything is now in place to compute the average speed of the train between neighboring stations:

// install

def speed[a, b] = transpose[^Distance][dv] / transpose[^Duration][tv] 
                  from dv in distance[a, b], tv in travel_time[a, b]
// query

def output = speed
Loading q-speed...

Full Generality

In general, a value type can be represented by a tuple. For example, you might want to associate a point in three-dimensional space with its three Cartesian coordinates. This can be done as follows:

// install

value type Point = Float, Float, Float

def point = (:P1, ^Point[0.0, 1.0, 2.0])
def point = (:P2, ^Point[-3.0, -2.0, -1.0])

The relation ^Point is not binary:

// query

def output(x, y, z, v) = ^Point(x, y, z, v) and point(_, v)
Loading q-point-conversion...

Extracting the Representation of a Value (General Case)

Since ^Point is not binary, you cannot use transpose to make it easy to get the three floating-point numbers that represent a value of type Point. However, you can define a relation that moves the last element of each tuple to the first position:

// install

@inline
def rotate_right[R](x, y...) = R(y..., x)

You might even want to use a convenient wrapper: functions that convert a value of type Point to its coordinates. The code looks like this:

// install

@inline
def coordinates[p in Point] = rotate_right[^Point][p]

@inline
def coord1[p in Point] = first[coordinates[p]]

@inline
def coord2[p in Point] = second[coordinates[p]]

@inline
def coord3[p in Point] = last[coordinates[p]]

It is now easy to get the coordinates of points or to compute the distance between two points:

// query

def output = label, coordinates[p] from label, p where point(label, p)
Loading q-reverse-points...
// install

def distance[x in Point, y in Point] = 
    sqrt[squared[coord1[x] - coord2[x]] + 
         squared[coord2[x] - coord2[y]] +
         squared[coord3[x] - coord3[y]]]
// query

def output = distance[point[:P1], point[:P2]]
Loading q-points-distance...

Units of Measurement

The general form of value types allows you to associate quantities with units of measurement. For example, in the little application for toy trains it was implicitly assumed that distances were expressed in terms of some unit of distance, but that unit was not explicitly specified. The unit can become a part of the value type.

You can try it out without overwriting the previous definitions by using different names:

// install

def UnitOfDistance = :in ; :cm ; :km ; :mile
value type UDistance = UnitOfDistance, Float
def dist = (:A, :B, ^UDistance[:cm, 100.0])
def dist = (:B, :C, ^UDistance[:in,  50.0])

The results of displaying dist may seem surprising at first:

// query

def output = dist
Loading q-dist...

Why is the type “Mixed”? Things become more clear after displaying each of the distances separately:

// query

def output = dist[:A, :B]
Loading q-dist-ab...
// query

def output = dist[:B, :C]
Loading q-dist-bc...

The unit is a part of the type. It is worth knowing that the information about the unit is a compile-time artifact and is not stored in the database. The extra precision comes for free!

Distances in different units are not directly comparable:

// query

def output = equal(^UDistance[:cm, 100.0], ^UDistance[:cm, 100.0])
Loading q-eq-cm...
// query

def output = equal(^UDistance[:cm, 100.0], ^UDistance[:in, 100.0])
Loading q-eq-cm-in...

Although the unit field in a value of type UDistance is not stored explicitly, ^UDistance is a ternary relation. So the following works:

// install

def unit[d in UDistance] = first[rotate_right[^UDistance][d]]
def quantity[d in UDistance] = second[rotate_right[^UDistance][d]]
// query

def x = ^UDistance[:in,  50.0]
def output = unit[x]
Loading q-show-unit...
// query

def x = ^UDistance[:in,  50.0]
def output = quantity[x]
Loading q-show-value...

Declaring Value Types in Terms of Value Types

Value types are fully fledged types. In particular, value types can be used to define other value types.

This is made clear by the following example:

// query

value type A = Int
value type B = String
value type C = A, B
def output = ^C[^A[1], ^B["bb"]]
Loading q-show-value-value-pair...

It’s helpful to compare the above with the following:

// query

value type C = Int, String
def output = ^C[1, "bb"]
Loading q-show-value-pair...

When you look at the output, you will see that the representation of tuples is identical in both examples. In other words, you can use value types without incurring any additional runtime costs.

General Expressions in Declarations of Value Types

The general form of a value type declaration is quite similar to the definition of a relation. The body of the declaration can include expressions that constrain the kind of values that can be constructed.

For example, you can declare a value type Small that is represented by a pair consisting of a small integer and a short string. An attempt to construct a value of this type with a larger integer or a longer string will be unsuccessful. Here is a way to write this:

// query

value type Small(x in Int, y in String) { abs[x] < 4 and string_length[y] < 4 }

def output = ^Small[0, "abc"]; ^Small[5, "a"]; ^Small[3, "long"]; ^Small[3, "b"]
Loading q-value-small...

Empty Value Types

A value type need not have any representation. The declaration value type None is correct; the corresponding constructor relation, ^None, is empty.

The effect is similar to that of type Missing in Julia. The non-existing value can be used as an element in a tuple, but yields no result when applied in arithmetic operations:

// query

value type None
def none = ^None[]
def output:tuple = 1, none, "three"
def output:mult = none * 1
Loading q-value-empty...

In fact, Rel already has a built-in type that is somewhat similar. As in Julia, it’s called Missing, and the non-existent value has the name missing.

// query

def output:tuple = 1, missing, "three"
def output:mult = missing * 1
Loading q-value-missing...

The type Missing is currently not a value type and the relation ^Missing does not exist.