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
ActorRecord
that has two fields: the string fieldname
and the unsigned integer fieldage
. The data contained by a record of this type is exactly the same as that contained by an instance of theActor
class 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
timestampDefault
is initialized withTimeStamp
’s default initializer. The expressionnew TimeStamp
creates a record that is assigned totimestampCustom
. It effectively initializestimestampCustom
via 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 (paramPassing.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.color
affects only the record stored inmc1
. The record inmc2
is not affected by the assignment tomc1
or by the assignment inprintMyColor
.mc2
is affected by the assignment inmodifyMyColor
because the intentinout
is 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.
Advanced Copy Initialization¶
A copy initializer can also be used to specify how a record should be initialized from a value of an arbitrary type. This kind of copy initializer is invoked when a variable declaration’s initialization expression is not of the same type as the record being initialized. For example:
record MyString {
var s : string;
}
// normal copy initializer
proc MyString.init=(other: MyString) {
this.s = other.s;
writeln("normal init=");
}
// initialize from a string
proc MyString.init=(other: string) {
this.s = other;
writeln("string init=");
}
var A = new MyString("hello");
var B = A; // "normal init="
var C : MyString = "goodbye"; // "string init="
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:
record Wrapper {
type T;
var x : T;
}
// normal copy initializer
proc Wrapper.init=(other: this.type) { ... }
// 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;
}
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 recordA
is created and initialized to all zeroes. Then, itsi
field is set to3
. For the second call toR.print
, the recordC
is created assigned toA
. SinceC
is default-initialized to all zeroes, those zero values overwrite both values inA
.The next clause demonstrates that
A
andC
are distinct entities, rather than two references to the same object. Assigning3.14
toC.x
does not affect thex
field 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 D: domain(R); var r = new R(42); 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
.