Special Methods

View specialMethods.chpl on GitHub

This primer covers special methods for classes and records

Classes and records can have a handful of methods that are specially named and treated differently than other methods. These methods enable operations such as initializing or deinitializing a class or record instance, accessing a class or record as though it were an array, iterating over an class or record, and defining the way a class or record is read from or written to a channel.

Note that there are two ways to declare a method. Methods declared within a class or record are called primary methods:

record ExampleRecord1 {
  var exampleField: int;
  proc primaryMethod() { }
}

Methods declared outside of a class or record are called secondary methods:

record ExampleRecord2 {
  var exampleField: int;
}
proc ExampleRecord2.secondaryMethod() { }

Methods declared outside of a class or record and outside of the scope where the type is defined are called tertiary methods. Tertiary methods follow the same declaration syntax as secondary methods.

This primer will use the secondary method form, but all of the special methods can also be written as primary methods.

First we will declare a simple record with a field that is a tuple of integers. We’ll add special methods and iterators to this record later.

record R {
  param size: int = 10;
  var vals: size*int;
}

Initializers and Deinitializers

An initializer named init is called when creating an instance of the class or record, for example with the new keyword. An initializer accepting zero arguments is called a default initializer.

A method named init= is called a copy initializer and accepts a single argument.

If a method named postinit that accepts zero arguments exists for a class or record type, it will automatically be called after the initializer call completes.

The deinit method will deinitialize a record when it leaves scope, or a class when delete is called on it. If the class or record contained any unmanaged classes, open files, etc. this method would be the place to delete them or otherwise clean them up. See the Records primer for more details on initializers and deinitializers.

The this Accessor

The this method gives the record the ability to be accessed like an array. Here we use the argument as an index to choose a tuple element.

proc R.this(n: int) ref {
  if !vals.indices.contains(n) then
    halt("index out of bounds accessing R");
  return vals[n];
}

All functions and methods in Chapel can be called using either parenthesis or square brackets. Here we’ll access the record by implicitly calling the this method defined above.

var r = new R();

r[0] = 1;
r(2) = 3;

writeln(r.vals);

Default Iterators

An iterator named these that can accept zero arguments is automatically called when a record or class instance is used in the iterator position of a for loop.

iter R.these() ref {
  for i in vals.indices {
    yield vals[i];
  }
}

for val in r {
  val += 1;
}
writeln(r.vals);

Classes and records can also define parallel iterators including leader/follower iterator pairs and standalone parallel iterators. For more information on parallel iterators, see the Parallel Iterators primer.

Custom Hashing

By default, the compiler will define a hash method for any record that does not define its own == or != overloads. This permits such records to be used as the indices of associative domains, the values in a Set, or the keys in a Map. Users can override this default by supplying their own hash method that returns a uint or int value. For example:

use Map;

proc R.hash(): int {
  writeln("In custom hash function");
  return vals[0];
}

Now that the record R has a hash method defined, Chapel’s, set, map, and associative domain types will call this custom hash instead of the compiler-generated method.

var myMap = new map(R, int);
var myD: domain(R);
var myR = new R();

myMap[myR] = 5;
myD += myR;

IO Methods

The writeThis method defines how to write an instance of R to a channel. We’ll write the vals tuple between asterisks. See section The readThis() and writeThis() Methods for more information on the writeThis and readThis methods.

use IO; // required for file operations

config const filename = "tempfile.txt";

proc R.writeThis(ch: fileWriter) throws {
  ch.write("*", vals, "*");
}

{
  // Open the file in a new block so that deinitializers
  // will close it at the end of the block
  var f = open(filename, iomode.cw);
  var ch = f.writer();
  ch.writeln(r);
}

The readThis method defines how to read an instance of R from a channel. We’ll read the vals tuple between asterisks like how it was written above.

proc R.readThis(ch: fileReader) throws {
  var star = new ioLiteral("*");
  ch.read(star);
  ch.read(vals);
  ch.read(star);
}

{
  var f = open(filename, iomode.r);
  var ch = f.reader();
  var r2 = new R();
  ch.readln(r2);
  assert(r == r2);
}

{
  var chW = openwriter(filename);
  chW.writeln(r);
  chW.flush();

  writeln(r);
  var r2 = new R();
  var chR = openreader(filename);
  chR.readln(r2);
  assert(r == r2);
}

Clean up the temporary file we created earlier.

{
  use FileSystem;
  if exists(filename) then
    remove(filename);
}

Operator Overloads

Operators can be overloaded for record types to support assignment (=), comparisons, (<, <=, >, >=, ==, !=), and other general operators (+, -, *, /, …). These are declared as regular functions using the operator keyword.