Skip to content
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.

Was this doc helpful?