Records
A record is a data structure that is similar to a class except it has value semantics, similar to primitive types. Value semantics mean that assignment, argument passing and function return values are by default all done by copying. Value semantics also imply that a variable of record type is associated with only one piece of storage and has only one type throughout its lifetime. Storage is allocated for a variable of record type when the variable declaration is executed, and the record variable is also initialized at that time. When the record variable goes out of scope, or at the end of the program if it is declared at module scope, it is deinitialized and its storage is deallocated.
A record declaration statement creates a record type Record Declarations. A variable of record type contains all and only the fields defined by that type (Record Types). Value semantics imply that the type of a record variable is known at compile time (i.e. it is statically typed).
A record can be created using the new operator, which allocates
storage, initializes it via a call to a record initializer, and returns
it. A record is also created upon a variable declaration of a record
type.
A record type is generic if it contains generic fields. Generic record types are discussed in detail in Generic Types.
Record Declarations
A record type is defined with the following syntax:
record-declaration-statement:
simple-record-declaration-statement
external-record-declaration-statement
simple-record-declaration-statement:
'record' identifier { record-statement-list }
record-statement-list:
record-statement
record-statement record-statement-list
record-statement:
variable-declaration-statement
method-declaration-statement
type-declaration-statement
empty-statement
A record-declaration-statement defines a new type symbol specified
by the identifier. As in a class declaration, the body of a record
declaration can contain variable, method, and type declarations.
If a record declaration contains a type alias or parameter field, or it contains a variable or constant field without a specified type and without an initialization expression, then it declares a generic record type. Generic record types are described in Generic Types.
If the extern keyword appears before the record keyword, then an
external record type is declared. An external record is used within
Chapel for type and field resolution, but no corresponding backend
definition is generated. It is presumed that the definition of an
external record is supplied by a library or the execution environment.
See the chapter on interoperability
(Interoperability) for more information on
external records.
Note
Future:
Privacy controls for classes and records are currently not specified, as discussion is needed regarding its impact on inheritance, for instance.
Record Types
A record type specifier simply names a record type, using the following syntax:
record-type:
identifier
identifier ( named-expression-list )
A record type specifier may appear anywhere a type specifier is permitted.
For non-generic records, the record name by itself is sufficient to specify the type. Generic records must be instantiated to serve as a fully-specified type, for example to declare a variable. This is done with type constructors, which are defined in Section The Type Constructor.
Record Fields
Variable declarations within a record type declaration define fields within that record type. The presence of at least one parameter field causes the record type to become generic. Variable fields define the storage associated with a record.
Example (defineActorRecord.chpl).
The code
record ActorRecord { var name: string; var age: uint; }defines a new record type called
ActorRecordthat has two fields: the string fieldnameand the unsigned integer fieldage. The data contained by a record of this type is exactly the same as that contained by an instance of theActorclass defined in the preceding chapter Class Fields.
Record Methods
A record method is a function or iterator that is bound to a record. See the methods section Methods for more information about methods.
The receiver of a record method is passed by const intent by default.
A method that modifies this must declare an explicit this-intent of
ref, see The Method Receiver and the this Argument.
Nested Record Types
A record defined within another class or record is a nested record. A nested record can be referenced only within its immediately enclosing class or record.
Record Variable Declarations
A record variable declaration is a variable declaration using a record type. When a variable of record type is declared, storage is allocated sufficient to store all of the fields defined in that record type.
In the context of a class or record or union declaration, the fields are allocated within the object as if they had been declared individually. In this sense, records provide a way to group related fields within a containing class or record type.
In the context of a function body, a record variable declaration causes storage to be allocated sufficient to store all of the fields in that record type. The record variable is initialized with a call to an initializer (Class Initializers) that accepts zero actual arguments.
Storage Allocation
Storage for a record variable directly contains the data associated with the fields in the record, in the same manner as variables of primitive types directly contain the primitive values. Unlike class variables, the field data of one record variable is not shared with data of another record variable.
Note that the storage for a record’s field does not necessarily directly contain all of the data stored in a type. In particular, a record with a field of array type actually stores a kind of array descriptor that points to memory for the elements elsewhere (see Runtime Representation of Array Values).
Record storage is reclaimed automatically. See Variable Lifetimes for details on when a record becomes dead.
Record Initialization
When default initializing a record (see Variable Lifetimes), an
init method on the record will be called. For a concrete record,
init wil be called with no arguments. For an instantiated generic
record, the type and param arguments are passed by name.
The compiler-generated default initializer for a record is defined in the same way as the default initializer for a class (The Compiler-Generated Initializer).
Records containing fields without types or fields with generic types (see Fields without Types and Fields with Generic Types) cannot be default-initialized.
To create a record as an expression, i.e. without binding it to a
variable, the new operator is required. In this case, storage is
allocated and reclaimed as for a record variable declaration
(Storage Allocation), except that the temporary record goes
out of scope at the end of the enclosing block.
The initializers for a record are defined in the same way as those for a class (Class Initializers). Note that records do not support inheritance and therefore the initializer rules for inheriting classes (Initializing Inherited Classes) do not apply to record initializers.
Example (recordCreation.chpl).
The program
record TimeStamp { var time: string = "1/1/1011"; } var timestampDefault: TimeStamp; // use the default for 'time' var timestampCustom = new TimeStamp("2/2/2022"); // ... or a different one writeln(timestampDefault); writeln(timestampCustom); var idCounter = 0; record UniqueID { var id: int; proc init() { idCounter += 1; id = idCounter; } } var firstID : UniqueID; // invokes zero-argument initializer writeln(firstID); writeln(new UniqueID()); // create and use a record value without a variable writeln(new UniqueID());produces the output
(time = 1/1/1011) (time = 2/2/2022) (id = 1) (id = 2) (id = 3)The variable
timestampDefaultis initialized withTimeStamp’s default initializer. The expressionnew TimeStampcreates a record that is assigned totimestampCustom. It effectively initializestimestampCustomvia a call to the initializer with desired arguments. The records created withnew UniqueID()are discarded after they are used.
As with classes, the user can provide their own initializers (User-Defined Initializers). If any user-defined initializers are supplied, the default initializer cannot be called directly.
Record Deinitializer
A record author may specify additional actions to be performed before
record storage is reclaimed by defining a record deinitializer. A record
deinitializer is a method named deinit(). A record deinitializer
takes no arguments (aside from the implicit this argument). If
defined, the deinitializer is called on a record object after it goes
out of scope and before its memory is reclaimed.
Example (recordDeinitializer.chpl).
class C { var x: int; } // A class with nonzero size. // If the class were empty, whether or not its memory was reclaimed // would not be observable. // Defines a record implementing simple memory management. record R { var c: unmanaged C; proc init() { c = new unmanaged C(0); } proc deinit() { delete c; } } proc foo() { var r: R; // Initialized using default initializer. writeln(r); // r will go out of scope here. // Its deinitializer will be called to free the C object it contains. } foo();
Record Arguments
Record formal arguments with the in intent will be copy-initialized
into the function’s formal argument
(Copy Initialization of Records).
Record formal arguments with inout or out intent will be updated
by the record assignment function (Record Assignment).
Example (argPassing.chpl).
The program
record MyColor { var color: int; } proc printMyColor(in mc: MyColor) { writeln("my color is ", mc.color); mc.color = 6; // does not affect the caller's record } var mc1: MyColor; // 'color' defaults to 0 var mc2: MyColor = mc1; // mc1's value is copied into mc2 mc1.color = 3; // mc1's value is modified printMyColor(mc2); // mc2 is not affected by assignment to mc1 printMyColor(mc2); // ... or by assignment in printMyColor() proc modifyMyColor(inout mc: MyColor, newcolor: int) { mc.color = newcolor; } modifyMyColor(mc2, 7); // mc2 is affected because of the 'inout' intent printMyColor(mc2);produces
my color is 0 my color is 0 my color is 7The assignment to
mc1.coloraffects only the record stored inmc1. The record inmc2is not affected by the assignment tomc1or by the assignment inprintMyColor.mc2is affected by the assignment inmodifyMyColorbecause the intentinoutis used.
Record Field Access
A record field is accessed the same way as a class field (Field Accesses). When a field access is used as an rvalue, the value of that field is returned. When it is used as an lvalue, the value of the record field is updated.
Accessing a parameter or type field returns a parameter or type, respectively. Also, parameter and type fields can be accessed from an instantiated record type in addition to from a record value.
Field Getter Methods
As in classes, field accesses are performed via getter methods (Field Getter Methods). By default, these methods simply return a reference to the specified field (so they can be written as well as read). The user may redefine these as needed.
Record Method Calls
Record method calls are written the same way as other method calls (Method Calls). Unlike class methods, record methods are always resolved at compile time.
Common Operations
Copy Initialization of Records
When a new record variable is created based upon an existing variable,
it is copy initialized or move initialized as described in
Copy and Move Initialization. When a record is copy initialized,
its init= initializer will be used to create the new record.
Copy initialization is implemented by a method named init=, known as the
copy initializer. A copy initializer may only accept one argument, which
represents the value from which the record will be initialized. These methods
share the same rules as a normal initializer (Class Initializers), along
with some additional restrictions.
The compiler-generated copy initializer for a non-generic record accepts an argument of the same type and simply initializes each field from the argument’s corresponding field:
record R {
var x, y, z: int;
}
// identical to compiler-generated implementation
// proc R.init=(other: R) {
// this.x = other.x;
// this.y = other.y;
// this.z = other.z;
// }
In order to override the compiler-generated implementation, the user must
implement an init= method with the same signature.
proc R.init=(other: R) {
this.x = other.x;
this.y = other.y;
this.z = other.z;
writeln("copied R!");
}
Note
If a user implements their own init= method, they must also implement an
assignment operator for the same record type. Implementing one without the
other will cause the compiler to issue an error. Rationale: this
requirement exists to mitigate hard-to-debug problems by requiring that type
authors take responsibility for both init= and = implementations, or
neither implementation.
A user may indicate that a type is not copyable by adding a where-clause to
the init= implementation that evaluates to false:
proc R.init=(other: R) where false {
}
The compiler-generated copy initializer for a generic type uses the expression
this.type as the argument’s type to ensure that the types of the original
record and its copy are the same:
record G {
type T;
var x : T;
}
// compiler-generated init= for 'G'
// proc G.init=(other: this.type) {
// this.T = other.T;
// this.x = other.x;
// }
Note that the generic fields must still be manually initialized, despite the type already being known. Future work may allow these fields to be inferred.
Mixed-Type Copy Initialization
A copy initializer can also be used to specify how a record should be initialized from a value of a distinct type. This kind of mixed-type copy initializer is invoked when a variable declaration’s initialization expression is not of the same type as the record being initialized.
Defining a mixed-type copy initializer like this also requires defining a cast operator that converts from the argument type to the record type.
Rationale: Supporting a mixed-type copy initializer provides a way
to convert an expression of one type (T1) into another (T2)
using forms like: var myT1: T1 = myT2;. The other common way of
converting types in this way is to use a cast, like myT2: T1, so
Chapel requires both operations as a means of ensuring the type author
makes both forms available.
As an example:
Example (copyInitMixedTypes.chpl).
record MyString { var s : string; } // normal copy initializer proc MyString.init=(other: MyString) { this.s = other.s; writeln("normal init="); } // mixed-type copy initializer, from a string proc MyString.init=(other: string) { this.s = other; writeln("string init="); } // the required cast operator (which can optionally be implemented // in terms of the copy initializer, as is done here) operator :(x: string, type t: MyString) { writeln("cast"); const s: MyString = x; return s; } var a = new MyString("hello"); var b = a; // prints "normal init=" var c: MyString = "goodbye"; // prints "string init=" var d = "goodbye": MyString; // prints "cast" and "string init=' due to the implementation
Generic types can rely on the this.type expression to implement these kinds
of copy initializers with the desired type constraints. The this.type
expression will evaluate to the type provided by the user at the variable
declaration:
Example (copyInitGeneric.chpl).
record Wrapper { type T; var x: T; } // normal copy initializer proc Wrapper.init=(other: this.type) { this.T = other.T; this.x = other.x; } // An incorrect attempt: ignores the user-specified type, and uses the // value's type (which might not be the same!) // i.e. 'var w: Wrapper(int) = "hi"', tries to create a 'Wrapper(string)' // proc Wrapper.init=(other: ?T) { // this.T = T; // this.x = other; // } // initialize a Wrapper from the desired wrapped type 'T' proc Wrapper.init=(other: this.type.T) { this.T = other.type; this.x = other; } // the required cast due to the presence of a mixed-type 'init=' operator :(x, type t: Wrapper(x.type)) { var w: Wrapper(x.type) = x; return w; } var a: Wrapper(int) = 4; var b: Wrapper(string) = "hello";
Record Assignment
A variable of record type may be updated by assignment. The compiler
provides a default assignment operator for each record type R having
the signature:
operator =(ref lhs:R, rhs:R) : void where lhs.type == rhs.type
In it, the value of each field of the record on the right-hand side is assigned to the corresponding field of the record on the left-hand side.
The compiler-provided assignment operator may be overridden as described in Assignment Statements.
The following example demonstrates record assignment.
Example (assignment.chpl).
record R { var i: int; var x: real; proc print() { writeln("i = ", this.i, ", x = ", this.x); } } var A: R; A.i = 3; A.print(); // "i = 3, x = 0.0" var C: R; A = C; A.print(); // "i = 0, x = 0.0" C.x = 3.14; A.print(); // "i = 0, x = 0.0"Prior to the first call to
R.print, the recordAis created and initialized to all zeroes. Then, itsifield is set to3. For the second call toR.print, the recordCis created assigned toA. SinceCis default-initialized to all zeroes, those zero values overwrite both values inA.The next clause demonstrates that
AandCare distinct entities, rather than two references to the same object. Assigning3.14toC.xdoes not affect thexfield inA.
Default Comparison Operators
Default functions to overload comparison operators are defined for
records if none are explicitly defined. == and != functions have the
following signatures for a record R:
operator ==(lhs:R, rhs:R) : bool where lhs.type == rhs.type;
operator !=(lhs:R, rhs:R) : bool where lhs.type == rhs.type;
Other comparison operator overloads (namely <, <=, >, and >=)
have similar signatures but their where clauses also check whether the relevant
operator is supported by each field.
Record comparisons have a similar behavior to tuple comparisons. The operators >, >=, <, and <=
check the corresponding lexicographical order based on pair-wise comparisons
between the arguments’ fields. The operators == and != check whether
the two arguments are pair-wise equal or not. The fields are compared in the
order they are declared in the record definition.
Hashing a Record
For any record that does not have a user-defined == or !=
operator, the compiler will automatically define a default hash method
for it. This allows values of that record type to be used as the
indices of an associative domain, the elements of a set, or the keys
of a map. The user can override this default hash method (or provide
one in cases that the compiler does not) by defining their own method
named hash on the record which takes no arguments and returns a
uint. To make the compiler aware of the hash method, the record
must be made to implement the hashable interface.
Example (userhash.chpl).
record R : hashable { var i: uint; proc hash(): uint { writeln("In custom hash function"); return i; } } // Creating an associative domain with an 'idxType' of 'R' // invokes R.hash() as part of its implementation var r = new R(42); const D = {r}; writeln(D);
Note that the compiler-generated hash can only be overridden for
records that have been defined in user code. As an result, this
feature cannot be used to override the default hash for built-in types
like int.
Differences between Classes and Records
The key differences between records and classes are listed below.
Declarations
Syntactically, class and record type declarations are identical, except
that they begin with the class and record keywords,
respectively. In contrast to classes, records do not support
inheritance.
Storage Allocation
For a variable of record type, storage necessary to contain the data
fields has a lifetime equivalent to the scope in which it is declared.
No two record variables share the same data. It is not necessary to call
new to create a record.
By contrast, a class variable contains only a reference to a class
instance. A class instance is created through a call to its new
operator. Storage for a class instance, including storage for the data
associated with the fields in the class, is allocated and reclaimed
separately from variables referencing that instance. The same class
instance can be referenced by multiple class variables.
Assignment
Assignment to a class variable is performed by reference, whereas assignment to a record is performed by value. When a variable of class type is assigned to another variable of class type, they both become names for the same object. In contrast, when a record variable is assigned to another record variable, then contents of the source record are copied into the target record field-by-field.
When a variable of class type is assigned to a record, matching fields (matched by name) are copied from the class instance into the corresponding record fields. Subsequent changes to the fields in the target record have no effect upon the class instance.
Assignment of a record to a class variable is not permitted.
Arguments
Record arguments use the const abstract intent by default.
Similarly, the this receiver argument is passed by const by default.
See The Default Intent and The Method Receiver and the this Argument.
No nil Value
Records do not provide a counterpart of the nil value. A variable of
record type is associated with storage throughout its lifetime, so
nil has no meaning with respect to records.
The delete operator
Calling delete on a record is illegal.
Default Comparison Operators
For records, the compiler will supply default comparison operators if
they are not supplied by the user. In contrast, the user cannot redefine
== and != for classes. The default comparison operators for a
record examine the arguments’ fields, while the comparison operators for
classes check whether the l.h.s. and r.h.s. refer to the same class
instance or are both nil.