THIS POST IS ARCHIVED

Value Types in Rel

Differentiating between types of values is an essential aspect of application development, as it enables clarity of modeling and leads to fewer errors.

Consider a database that stores distances and travel times between different cities. For example:

def distance = ("Tampa", "Miami", 279.9)
def distance = ("Miami", "Atlanta",  662.6)
 
def travel_time = ("Tampa", "Miami", 5)
def travel_time = ("Miami", "Atlanta", 11)

Although this data describes the scenario reasonably well, it may lead to issues when querying the data. Specifically, distances and time intervals are represented here as plain numbers even though they represent quantities denominated in different units, so it's possible to confuse the two when querying.

For example, the following query produces results even though they are not meaningful:

def length[x, y] = distance[x, y] + travel_time[x, y]
def output = length
def distance = ("Tampa", "Miami", 279.9)
def distance = ("Miami", "Atlanta",  662.6)
 
def travel_time = ("Tampa", "Miami", 5)
def travel_time = ("Miami", "Atlanta", 11)
def length[x, y] = distance[x, y] + travel_time[x, y]
def output = length

Output:

In this case distance (in miles) was accidentally added to travel time (in hours), thus producing unhelpful output. Ideally, users should be protected from these kinds of mistakes.

We are excited to announce the support of value types in Rel, which allow users to distinguish elements that represent different things when writing applications in Rel.

You can define a value type as follows:

value type Distance = Float
value type Duration = Int

These declarations define two types, one for travel distance and one for travel duration. A value type has its own name and is distinct from every other type, even though it may be parameterized by common underlying types.

Value types are useful as they allow users to make their models clearer, more readable, and more maintainable. They also help avoid mistakes similar to the one presented above.

Value types allow users to distinguish between different kinds of values, regardless of the potentially identical underlying representation. Additionally, value types can be used to define other value types under certain conditions.

Defining Value Types

Using value types, the previous example can now be rewritten:

value type Distance = Float
value type Duration = Int
 
def distance = ("Tampa", "Miami", ^Distance[279.9])
def distance = ("Miami", "Atlanta", ^Distance[662.6])
 
def travel_time = ("Tampa", "Miami", ^Duration[5])
def travel_time = ("Miami", "Atlanta", ^Duration[11])

In this example, the partial application ^Distance[279.9] evaluates to the value of type Distance that corresponds to the number 279.9. Given the use of value types, the relation distance is now represented as follows:

def output = distance
value type Distance = Float
value type Duration = Int
 
def distance = ("Tampa", "Miami", ^Distance[279.9])
def distance = ("Miami", "Atlanta", ^Distance[662.6])
 
def travel_time = ("Tampa", "Miami", ^Duration[5])
def travel_time = ("Miami", "Atlanta", ^Duration[11])
def output = distance

Output:

The third column now contains values of type Distance.

Value types need not be parameterized by individual values; they can be parameterized by tuples. For example, here is a value type Point3d that represents a point in a three dimensional space:

value type Point3d = Float, Float, Float

Value types also support expressions in their declaration. For example, here is a value type Age, which is meant to represent a person's age:

value type Age(x in Int) { 0 <= x and x <= 120 } 

def output = ^Age[15] ; ^Age[150]

Output:

Value types don't necessarily represent infinite relations as we saw with the Distance relation above.

As an example, consider the following two value type definitions. The first specifies the days of the week, while the second defines a boolean value type which is parameterized by an 8-bit integer constrained to be either 0 or 1:

value type Weekday = 1; 2; 3; 4; 5; 6; 7

value type Bool(x) = Int[8](x) and (x = 0 or x = 1)

Additionally, a value type does not necessarily need a representation or an underlying type. For example, the following declaration specifies a value type called None that has the empty constructor relation ^None.

value type None

def none = ^None[]

def output:tuple = 1, none, "str"
def output:mul = none * 10

Output:

Value types can also be parameterized by other value types. Here is an example that first defines the value type DistanceUnit with distance units and then defines another value type Distance that uses DistanceUnit.

value type DistanceUnit = :Meters; :Miles
value type Distance = DistanceUnit, Int

def output = ^Distance[^DistanceUnit[:Meters], 10]

Output:

Defining value types that build upon other value types allows for greater expressivity and clarity in the applications being created.

Performing Operations Using Value Types

The definitions presented so far show how to describe different types and construct data that adhere to those value types.

In order to use and perform operations on the data of a given value type, the associated data need to be extracted to perform the operation accordingly.

Consider again one of the first examples using the Distance value type:

value type Distance = Float
value type Duration = Int
 
def distance = ("Tampa", "Miami", ^Distance[279.9])
def distance = ("Miami", "Atlanta", ^Distance[662.6])

In order to find the distance from Tampa to Atlanta through Miami, the two distances need to be added together. This operation can be performed by extracting the two floating point numbers from ^Distance:

def float_from_distance(d in Distance, x) = ^Distance(x, d)

def output { 
    float_from_distance[distance["Tampa", "Miami"]] 
    + float_from_distance[distance["Miami", "Atlanta"]]
}
value type Distance = Float
value type Duration = Int
 
def distance = ("Tampa", "Miami", ^Distance[279.9])
def distance = ("Miami", "Atlanta", ^Distance[662.6])
def float_from_distance(d in Distance, x) = ^Distance(x, d)

def output { 
    float_from_distance[distance["Tampa", "Miami"]] 
    + float_from_distance[distance["Miami", "Atlanta"]]
}

Output:

We are also excited to announce support for operators over value types. Specifically, the same addition operation used over the Distance value type can also be defined as an operator.

Operators can be applied over data of specific value types to perform computations depending on need. Here is the same example where an addition + operator is defined:

def (+)[x in Distance, y in Distance] =
    ^Distance[float_from_distance[x] + float_from_distance[y]]

def output = distance["Tampa", "Miami"] + distance["Miami", "Atlanta"]
value type Distance = Float
value type Duration = Int
 
def distance = ("Tampa", "Miami", ^Distance[279.9])
def distance = ("Miami", "Atlanta", ^Distance[662.6])
def float_from_distance(d in Distance, x) = ^Distance(x, d)
def (+)[x in Distance, y in Distance] =
    ^Distance[float_from_distance[x] + float_from_distance[y]]

def output = distance["Tampa", "Miami"] + distance["Miami", "Atlanta"]

Output:

In summary, value types help distinguish between different kinds of values, even though the underlying data may be identical. Value types can be used to define other value types.

For more details please see the Value Types Concept Guide.