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:

``````// model

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:

``````// read query

def output[x, y] = distance[x, y] + travel_time[x, y]``````

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 following declaration introduces a new type, `Distance`, whose values are represented by floating-point numbers but are distinct from values of all other types.

``value type Distance = Float``

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 use them only for constructors of value types and of entity types.

Starting from scratch, you rewrite the model:

``````// model

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`:

``````// read query

def output = distance``````

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

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

``````// read query

def output(x, y) = ^Distance(x, y) and distance(_, _, y)``````

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"`:

``````// read query

def output[x, y] = distance[x, y] + travel_time[x, y]``````

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`:

``````// read query

def output = distance[:A, :B] + distance[:B, :C]``````

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:

``````// model

def distance_to_float(d in Distance, x) = ^Distance(x, d)``````

You could achieve exactly the same effect by using the Standard Library relation transpose:

``def distance_to_float = transpose[^Distance]``

You can now list the distances as floating-point values, or add such floating-point values together:

``````// read query

def output[source, destination] =
distance_to_float[distance[source, destination]]``````
``````// read query

def output =
distance_to_float[distance[:A, :B]] + distance_to_float[distance[:B, :C]]``````

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

``````// model

def duration_to_int = transpose[^Duration]

def speed[a, b] =
distance_to_float[distance[a, b]] / duration_to_int[travel_time[a, b]]``````
``````// read query

def output = speed``````

You might also want to define appropriate operations for your value types, for example, addition of distances:

``````// model

def (+)[x in Distance, y in Distance] =
^Distance[distance_to_float[x] + distance_to_float[y]]``````
``````// read query

def output = distance[:A, :B] + distance[:B, :C]``````

## 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. You can do this as follows:

``````// model

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:

``````// read query

def output(x, y, z, v) = ^Point(x, y, z, v) and point(_, v)``````

### 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. The definition uses varargs:

``````// model

@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:

``````// model

@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:

``````// read query

def output = label, coordinates[p] from label, p where point(label, p)``````
``````// model

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

def output = distance[point[:P1], point[:P2]]``````

### 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:

``````// model

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:

``````// read query

def output = dist``````

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

``````// read query

def output = dist[:A, :B]``````
``````// read query

def output = dist[:B, :C]``````

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:

``````// read query

def output {^UDistance[:cm, 100.0] = ^UDistance[:cm, 100.0]}``````
``````// read query

def output {^UDistance[:cm, 100.0] = ^UDistance[:in, 100.0]}``````
``````// read query

def output {^UDistance[:cm, 254.0] = ^UDistance[:in, 100.0]}``````

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

``````// model

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

def x = ^UDistance[:in,  50.0]
def output = unit[x]``````
``````// read query

def x = ^UDistance[:in,  50.0]
def output = quantity[x]``````

### The Right-hand Side Specifies a Relation

The general form of a value type declaration is quite similar to the definition of a relation. The right-hand side of the declaration specifies an arbitrary relation that is the domain of the constructor.

Up to now, you have only seen domains that are type relations or products of type relations. But the mechanism is general.

For example:

``````// read query

def P = (1, 2); (3, 4); (3, 5)
def Q = "ab"; "cd"
value type V = P; Q

def output:A = ^V[1, 2]
def output:B = ^V["cd"]
def output:C = ^V["bc"]
def output:D = ^V[3]
def output:E = ^V[4]``````

Notice that `^V["bc"]` evaluates to `false`, because the string `"bc"` is not in the domain `P; Q`. Similarly, there is no tuple in the domain that begins with `4`, so there is no result for `^V[4]`.

The example above is not one to follow, but an illustration of the generality of value type declarations. Being aware of the generality may help you understand the unwanted effects of inadvertent mistakes.

Here is another example of this generality. You may want to have 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, but will not elicit a warning from Rel. Here is a way to write this:

``````// read 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"]``````

### Multiple Definitions of the Same Value Type

A value type can have multiple definitions. For example, you might want to create points from integers as well as from floats:

``````// read query

value type Point = Float, Float, Float
value type Point = Int, Int, Int

def switch[a in Point, b in Point] = b, a

def output = switch[^Point[1.0, 2.0, 3.0], ^Point[1, 2, 3]]``````

In the example above, the relation `switch` can have arguments of either kind of `Point`. But points created from floats will be different from those created from integers. For example, given the definitions above, the following will evaluate to `false`:

``def output { ^Point[1.0, 2.0, 3.0] = ^Point[1, 2, 3] }``

### 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.

However, the definitions cannot be directly or indirectly recursive.

Consider the following example:

``````// read query

value type A = Int
value type B = String
value type C = A, B
def output = ^C[^A[1], ^B["bb"]]``````

Compare the above with the following:

``````// read query

value type C = Int, String
def output = ^C[1, "bb"]``````

Even if it doesn’t seem so from the outputs, the representation of tuples is identical in both examples. In other words, you can use value types without incurring additional storage costs.

#### Definitions of Value Types Cannot Be Recursive

Section Multiple Definitions of the Same Value Type contained an example of two definitions of the value type `Point`. The second definition made it possible to create points from integer coordinates, but such points were different from points that were created from floating-point coordinates.

It is tempting to use a trick similar to the one that is common in the programming language Julia, and make the second definition perform a conversion to the canonical representation, that is, a triple of floating-point values:

``````value type Point = Float, Float, Float
value type Point(x in Int, y in Int, z in Int) {
Point[int_float_convert(x), int_float_convert(y), int_float_convert(z)]
}``````

This will not work, because Rel does not allow the definition of a value type to be recursive.

In general, allowing recursive definitions of value types would enable you to write, for example, the following definitions for a type that is intended to emulate linked lists of integers:

``````value type Nil
value type Cons = Int, Nil
value type Cons = Int, Cons``````

If this were legal, you would be able to construct lists of arbitrary length, for example:

``def output = ^Cons[ 2, ^Cons[1, ^Nil[]]]``

This cannot be allowed, because a value of a value type must have a fixed length that can be determined at compile time. Values whose lengths are not fixed could not be stored as elements of tuples.

### 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` (opens in a new tab) in Julia. The non-existing value can be used as an element in a tuple, but yields no result when applied in arithmetic operations:

``````// read query

value type None
def none = ^None[]
def output:tuple = 1, none, "three"
def output:mult = none * 1``````

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

``````// read query

def output:tuple = 1, missing, "three"
def output:mult = missing * 1``````

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

### Value Types in Modules

A value type defined in a module can be used as expected, with one exception that is described at the end of this section.

If a module is not parameterized, the value type can be declared in the module, used inside the module, and imported from the module. All these features can be illustrated by a simple example:

``````// read query

module A
value type V = Int
def v = ^V[7]
end

def output = A:v
def output = A:^V[8]
with A use ^V
def output = ^V[17]``````

If a module is parameterized, a value type can be declared in terms of the parameter, and can be imported from the module:

``````// read query

@inline
module A[R]
value type V = R
end

def output:one = A[Int][:^V][3]
def output:two = A[String][:^V]["hi!"]``````

However, if you use the value type within the module, then you must replace `@inline` with `@outline`:

``````// read query

@outline  // NOT @inline!
module A[R]
value type V = R
def v = ^V[top[2,R][_]]
end

def p = 1; 3; 5
def output:one = A[p][:v]
def output:two = A["A"; "B"; "C"][:v]``````

`@outline` must be used even if the value type does not depend on the parameter:

``````// read query

@outline  // NOT @inline
module A[R]
value type V = Int
def v = ^V[7]
end

def output = A[String][:v]``````

## Summary

In brief, value types allow you to distinguish between different kinds of values, even if they have the same underlying representation. This feature helps in avoiding some mistakes and making models easier to read.

Value types can be used to define other value types, as long as such definitions are not directly or indirectly recursive.