Records

View records.chpl on GitHub

This primer covers the declaration and uses of records.

A record is a type that can contain variables and constants, called fields, as well as functions and iterators called methods. Records have many similarities to classes, but there are several important differences:

  • classes support inheritance and virtual dispatch while records do not

  • different class variables can refer to the same instance, while record variables refer to distinct memory

  • copy-initialization and assignment can be implemented for records but not for classes.

A record type can be declared using the record keyword:

record Color {
  var red: uint(8);
  var green: uint(8);
  var blue: uint(8);
}

The new keyword creates a new record by calling an initializer. The record Color doesn’t declare any initializers, and so the compiler-generated one is used. The compiler-generated initializer has an argument for each field in the record.

The result of new can be captured in a variable:

var taupe = new Color(179, 139, 109);
writeln(taupe); // output: (red = 179, green = 139, blue = 109)

Alternatively, a variable can be declared using the record type without explicitly initializing it. In that event the variable is default-initialized. The compiler-generated initializer allows default initialization. It simply default-initializes each field in turn.

var myColor: Color;
writeln(myColor); // output: (red = 0, green = 0, blue = 0)

Records support methods, both declared within the record or declared outside of the record. Note that inside of a method, the fields may be accessed by name. Additionally this refers to the entire record.

proc Color.luminance() {
  return 0.2126*red + 0.7152*green + 0.0722*blue;
}
writeln(taupe.luminance());

Here is an example of a record to store a point where a method is declared inside of the record declaration. This record also demonstrates a few initializers.

record Point {
  var x: real;
  var y: real;
  var z: real;

  // This is a default initializer
  proc init() {
    writeln("Point default-init");
    this.x = 0.0;
    this.y = 0.0;
    this.z = 0.0;
  }

  // This is a useful initializer for this type
  proc init(x: real, y: real, z: real) {
    this.x = x;
    this.y = y;
    this.z = z;
  }

  // This is a copy initializer
  proc init=(from: Point) {
    writeln("Point copy-init");
    this.x = from.x;
    this.y = from.y;
    this.z = from.z;
  }
  proc magnitude() {
    return sqrt(x*x + y*y + z*z);
  }
}

Let’s create a default-initialized Point

var emptyPoint:Point; // outputs: Point default-init

Let’s create a point by calling the useful initializer and then call the magnitude method on it.

var myPoint = new Point(1.0, 2.0, 2.0);
writeln(myPoint.magnitude()); // outputs: 3.0

Since different record variables always refer to different records, the compiler will add calls to the record’s copy-initializer when initializing a new record variable from another record. For example:

var otherPoint = myPoint; // prints: Point copy-init
otherPoint.x = 5.0;
writeln(otherPoint); // outputs: (x = 5.0, y = 2.0, z = 2.0)

myPoint.x remains 1.0 since it has different storage.

writeln(myPoint); // outputs: (x = 1.0, y = 2.0, z = 2.0)

For similar reasons, when assigning between two record variables, the compiler will add calls to the record assignment operator. The function below defines the assignment operator for Point.

operator Point.= (ref lhs: Point, rhs: Point) {
  writeln("Point assignment");
  lhs.x = rhs.x;
  lhs.y = rhs.y;
  lhs.z = rhs.z;
}

Assignment between records of this type will invoke the above function:

otherPoint = myPoint; // prints: Point assignment

Records can also define a deinit method that will be called to clean up any resources used by the record when it goes out of scope.

proc Point.deinit() {
  writeln("Point deinit");
}

This block demonstrates when Point.deinit is called

{
  var tmpPoint = myPoint; // prints: Point copy-init
  // tmpPoint now goes out of scope
  // prints: Point deinit
}

Similarly to classes, records can define the special methods this and these.

The this method can make the record behave like a function or an array:

proc Point.this(i: int): real {
  if i == 1 then
    return x;
  if i == 2 then
    return y;
  if i == 3 then
    return z;

  return 0.0;
}

Now we can access the coordinates numerically instead of by name:

writeln(myPoint(1)); // outputs: 1.0

The this method enables both parenthesis-style access and square brace style:

writeln(myPoint[1]);

The these method is an iterator that the compiler will call when the record is looped over directly.

iter Point.these() {
  yield x;
  yield y;
  yield z;
}

Now we can easily loop over the coordinates in the point

for component in myPoint {
  writeln(component);
}