Partial Instantiations

Overview

A generic class or record can be fully instantiated, fully generic, or partially instantiated. A fully instantiated type expression has concrete types for all generic fields. A fully generic type expression is one in which none of the generic fields have been instantiated. A partially instantiated type expression has concrete types for one or more, but not all, generic fields. Partial instantiations can be used to more finely constrain a procedure’s arguments, constrain valid types when initializing variables, or for conveniently creating other instantiations through incremental instantiation.

Creating Partial Instantiations

A generic type is instantiated by a call to the type constructor, which accepts an argument for each generic field in the type in field declaration order. These arguments can be passed positionally or by using named expressions and the names of fields. In order to support partial instantiations of generic types the language allows for arguments to be omitted from the type constructor call. The resulting instantiation will have its own type constructor that accepts arguments for the remaining generic fields.

In order to make it clear that the result is a generic type, and to avoid confusion when an argument is accidentally missing from a type constructor, code should indicate that it is making a partial instantiation with a trailing ? argument. Otherwise, the compiler will issue a warning or error. The meaning of ? in this context is discussed further below.

For example:

record R {
  type X;
  type Y;
  param p : int;
}

type A = R; // 'A' is simply an alias for the generic type 'R'

// B's type constructor accepts two arguments for fields 'Y' and 'p'
type B = R(int, ?); // instantiate field 'X'
writeln(B:string); // "R(int(64))"

// C's type constructor accepts one argument for field 'Y'
type C = B(p=5, ?); // instantiate field 'p'
writeln(C:string); // "R(int(64),p=5)"

// D is a fully instantiated type and has no type constructor
type D = C(real); // instantiate field 'Y'
writeln(D:string); // "R(int(64),real(64),p=5)"

A generic field might depend on a previously declared field to describe its type. In such cases the later generic field may not be instantiated before the earlier generic field:

// dependent.chpl
record R {
  type T;
  param p : T;
}

type A = R(p=5);

Output:

dependent.chpl:7: error: Unable to resolve partial instantiation 'R(p=5)'
dependent.chpl:7: note: Instantiation of field 'p' depends on uninstantiated fields: 'type T'

Generic fields may have default values that are used to instantiate the field when the user does not explicitly provide an argument in the type constructor. In order to allow these fields to remain generic the user can pass ? as an argument to the type constructor. This argument indicates that generic fields with default values should not be automatically instantiated.

record MyRange {
  type idxType = int;
  param stridable = false;
}

// all arguments must be of type ``MyRange(int,false)``
proc foo(arg : MyRange) { ... }

// Any instantiation of ``MyRange`` is accepted
proc bar(arg : MyRange(?)) { ... }

// Any ``MyRange`` accepted provided the first field is ``uint``
proc baz(arg : MyRange(uint, ?)) { ... }

There are some restrictions that accompany the ? argument. First is that ? may only appear once in a type constructor call. A user might be tempted to use ? as a positional argument in order to more easily pass an argument to a specific field, e.g. R(?, ?, int). In order to reduce confusion and support ? as a convenient catch-all argument, ? may not be used multiple times in a type constructor call. For example:

record R {
  type X = int;
  type Y = real;
  type Z = string;
}

// Error! '?' may not be used multiple times in a type constructor call
// proc foo(arg : R(?, ?, uint)) { ... }
// instead, use named-expressions:
proc foo(arg : R(?, Z=uint)) { ... }

// alternatively, use the named-expression first:
proc bar(arg : R(Z=uint, ?)) { ... }

Furthermore, once ? appears in a type constructor call all later arguments in the same call must used named expressions. This restriction emphasizes that ? may not be used as a positional argument. For example:

record R {
  type A = int;
  type B = real;
}

// Error! second argument must use a named expression
// proc foo(arg : R(?, string)) { ... }
// instead...
proc foo(arg : R(?, B=string)) { ... }

Detecting Partial Instantiations

There are a few features that allow for the detection of partial instantiations.

The first feature is a standard library function named isGeneric that will simply return true if its argument is a partial instantiation or a fully-generic type:

record R {
  type T;
  param p : int;
}

writeln(isGeneric(R));            // true
writeln(isGeneric(R(int,?)));     // true
writeln(isGeneric(R(p=42, ?)));   // true
writeln(isGeneric(R(string, 5))); // false

Users may also query individual fields to determine whether the field has been instantiated by comparing the field against ?:

record R {
  type T;
  param p : int;
}

type A = R(int, ?);

// '?' can be used to compare against either type or param fields
writeln(A.T == ?); // false
writeln(A.p == ?); // true

Finally, a Reflection function named isFieldBound will return true if the given name of the field in the provided type has been instantiated:

use Reflection;

record R {
  type T;
  type U;
  param p : int;
}

proc printInstantiated(type T) {
  writeln("type = ", T:string);
  for param i in 0..<numFields(T) {
    param name : string = getFieldName(T, i);
    writeln("  field ", name, " = ", isFieldBound(T, name));
  }
}

printInstantiated(R);
printInstantiated(R(U=real, ?));

This program outputs:

type = R
  field T = false
  field U = false
  field p = false
type = R(U=real(64))
  field T = false
  field U = true
  field p = false

Passing and Returning Generic Types

As of the 1.20 release generic type expressions may be passed to and returned from functions. This change supports not only partial instantiations, but also generic management of class types.

class C {
  type T;
  param p : int;
}

proc defaultC() type {
  return C(int, 5)?;
}

proc intC type {
  return C(int, ?)?;
}

// Declare two variables with the same instantiation of 'C' but with different management
var x : unmanaged defaultC();
var y : owned defaultC();

// Declare two variables with different instantiations of 'C', based on a
// common instantiation returned by 'intC', and with different management
var a : owned intC(100);
var b : shared intC(123);

// An factory function that accepts any kind of type and creates a new
// instance with param 'p'
proc make(type T, param p : int) {
  return new T(p);
}

// For these calls, 'T' will be a partial instantiation
var q = make(C(int, ?), 1);
var r = make(C(real, ?), 2);