Types
Chapel is a statically typed language with a rich set of types. These include a set of predefined primitive types, enumerated types, structured types (classes, records, unions, tuples), data parallel types (ranges, domains, arrays), and synchronization types (sync, atomic).
The syntax of a type is as follows:
type-expression:
primitive-type
enum-type
structured-type
dataparallel-type
synchronization-type
lvalue-expression
if-expression
unary-expression
binary-expression
expression
Many expressions are syntactically allowed as a type; however not all expressions produce a type. For example, a call to a function is syntactically allowed as the type of a variable. However it would be an error for that call to result in a value (rather than a type) in that context.
Programmers can define their own enumerated types, classes, records, unions, and type aliases using type declaration statements:
type-declaration-statement:
enum-declaration-statement
class-declaration-statement
record-declaration-statement
union-declaration-statement
type-alias-declaration-statement
These statements are defined in Sections Enumerated Types, Class Declarations, Record Declarations, Union Declarations, and Type Aliases, respectively.
Primitive Types
The concrete primitive types are: void, nothing, bool,
int, uint, real, imag, complex, string and
bytes. They are defined in this section.
In addition, there are several generic primitive types that are described in Built-in Generic Types.
The primitive types are summarized by the following syntax:
primitive-type:
'void'
'nothing'
'bool'
'int' primitive-type-parameter-part[OPT]
'uint' primitive-type-parameter-part[OPT]
'real' primitive-type-parameter-part[OPT]
'imag' primitive-type-parameter-part[OPT]
'complex' primitive-type-parameter-part[OPT]
'string'
'bytes'
'enum'
'record'
'class'
'owned'
'shared'
'unmanaged'
'borrowed'
primitive-type-parameter-part:
( integer-parameter-expression )
integer-parameter-expression:
expression
If present, the parenthesized integer-parameter-expression must
evaluate to a compile-time constant of integer type.
See Compile-Time Constants
Open issue.
There is an expectation of future support for larger bit width primitive types depending on a platform’s native support for those types.
The Void Type
The void type is used to represent the lack of a value. It is
primarily used to indicate that a function does not return anything.
Example (returnVoid.chpl).
For example, the below declares
fto returnvoid:proc f() : void { }The compiler can infer the return type of
voidas well. See See Return Types for more information.
It is an error to assign the result of a function that returns void
to a variable.
The Nothing Type
The nothing type is used to indicate a variable or field that should
be removed by the compiler. The value none is the only value of type
nothing.
The value none can only be assigned to a variable of type
nothing, or to a generic variable that will take on the type
nothing. The variable will be removed from the program and have no
representation at run-time.
Rationale.
The
nothingtype can be used to conditionally remove a variable or field from the code based on aparamconditional expression.Example (noneNothing.chpl).
The
nothingtype andnonevalues typically come up in a generic programming context (see also Generics). For example, the following program defines a generic functiongthat can determine if it was called with an integer or withnone:proc g(arg) { if arg.type != nothing { writeln(arg); } } g(1); // outputs 1 g(none); // does not create output
The Bool Type
Chapel defines a logical data type designated by the symbol bool
with the two predefined values true and false. Values of this
boolean type are stored using an implementation-defined number of
bits.
Some statements require expressions of bool type and Chapel supports
a special conversion of values to bool type when used in this
context (Implicit Conversions for Conditionals).
Variables of type bool have a default value of false if they are
not initialized to something else (see also Variables).
Example (bools.chpl).
This program demonstrates creating a variable with type
booland setting it totrue, and then setting another variable to the logical negation of it:var x: bool = true; var y = !x; var z: bool;All three variables have type
bool. Note that the types ofxandyare optional; the program indicates the type ofxbut the compiler infers the type ofy. See Variables for more details. The last variable is initialized to the default value ofbool, which isfalse(see Default Initialization).
Signed and Unsigned Integral Types
The integral types can be parameterized by the number of bits used to
represent them. Valid bit-sizes are 8, 16, 32, and 64. The default
signed integral type, int, is a synonym for int(64); and the
default unsigned integral type, uint, is a synonym for uint(64).
Variables of integral type have a default value of 0 if they are
not initialized to something else (see also Variables).
The integral types and their ranges are given in the following table:
Type |
Minimum Value |
Maximum Value |
|---|---|---|
int(8) |
-128 |
127 |
uint(8) |
0 |
255 |
int(16) |
-32768 |
32767 |
uint(16) |
0 |
65535 |
int(32) |
-2147483648 |
2147483647 |
uint(32) |
0 |
4294967295 |
int(64), int |
-9223372036854775808 |
9223372036854775807 |
uint(64), uint |
0 |
18446744073709551615 |
Integer literals such as 3 have type int. However, such literals
can implicitly convert to other numeric types that can losslessly store
the value. See Implicit Compile-Time Constant Conversions.
Integer literals can be written in hexadecimal, octal, or binary. See
Literals.
Signed integral types of can implicitly convert to signed integral types of larger width. Additionally, signed integral types can implicitly convert to unsigned integral types of the same or larger width. Unsigned integral types can implicitly convert to both signed and unsigned integral type of larger width. See Implicit Numeric and Bool Conversions for details.
It is possible for overflow to occur with binary operators on integers. For signed integers, overflow leads to undefined behavior. For unsigned integers, overflow leads to wrapping since any bits not representable will be discarded.
Example (integers.chpl).
Here,
xis inferred to have typeint:var x = 1;and
yis initialized by converting2to auint(8):var y:uint(8) = 2;Then,
zis set to an expression that would evaluate to257, but that is not representable as auint(8), so it results in the wrapped value1.var z = 255 + y;
Real Types
Unlike integral types, real types are floating point types that can
store fractional values. Like the integral types, the real types can be
parameterized by the number of bits used to represent them. The default
real type, real, is 64 bits. The real types that are supported are
machine-dependent, but usually include real(32) (single precision)
and real(64) (double precision) following the IEEE 754 standard.
Variables of real type have a default value of 0.0 if they are
not initialized to something else (see also Variables).
All integral types can implicitly convert to all real types, and
real(32) can implicitly convert to real(64). See
Implicit Numeric and Bool Conversions for details.
real literals such as 5.2 have type real. However, such literals
can implicitly convert to other numeric types that can losslessly store
the value. See Implicit Compile-Time Constant Conversions.
real literals can be written in decimal or hexadecimal and with or
without an exponent (see Literals for details):
in decimal without an exponent, e.g.
5.2in decimal with an exponent, e.g.
6.02e23in hexadecimal without an exponent, e.g.
0x2.fein hexadecimal with a decimal exponent, e.g.
0x2.fep23Example (harmonic.chpl).
For example, this program computes the first
nterms of the harmonic series.First, it defines a
config constto allow setting the value ofnon the command line (see Variable Declarations):config const n = 100;Next, it declares a
realvariable. Since this variable isn’t initialized, it will be initialized to 0.0:var sum:real;Then, it loops over the first n elements and adds them to the sum (see also The For Loop):
for i in 1..n { sum += 1.0/i; }Note that it uses 1.0/i in order to do a floating point division. If it used 1/i, it would do integer division (rounding towards zero), which evaluates to
0fori > 1.Finally, it prints out the sum:
writeln(sum);
Imaginary Types
Imaginary types are floating-point types, and similarly to real
types, they can be parameterized by the number of bits used to
represent them. The default imaginary type, imag, is 64 bits. The
imaginary types that are supported are machine-dependent, but usually
include imag(32) and imag(64).
Rationale.
The imaginary type is included to avoid numeric instabilities and under-optimized code stemming from always converting real values to complex values with a zero imaginary part.
Imaginary literals can be created by appending i to a numeric
literal; for example, 0.6i. Such literals have type imag.
However, such literals can implicitly convert to other numeric types that
can losslessly store the value. See
Implicit Compile-Time Constant Conversions. As with real
literals, imaginary literals can be written in decimal or hexadecimal and
with or without an exponent (see Literals for details):
Variables of imag type have a default value of 0.0i if they are
not initialized to something else (see also Variables).
It is possible to convert between a real value and an imag value
using an explicit cast (see Explicit Conversions). Similarly, an
imag value can be cast to a real value. Such casts preserve the
floating-point value while changing whether or not it is imaginary.
Example (imaginary.chpl).
For example, this program creates imaginary numbers in two different ways. First,
ais animagvariable initialized to a literal:var a = 0.6i;Now, suppose we have a
realvalues:var s = 10.25;We can initialize an
imagvariable with the same numeric value, but as an imaginary value, with a cast:var b = s:imag; assert(b == 10.25i);
Complex Types
The complex type represents a complex number. A complex value has
floating-point values for the real and imaginary components.
As with the integral and real types, the type complex can be
parameterized by the number of bits used to represent the complex number.
Since the complex number consists of two components, the number of bits
used to represent it is twice the number of bits used to represent each
component.
In particular:
complex(64)contains tworeal(32)fields
complex(128)contains tworeal(64)fields
The real and imaginary components can be accessed via the methods re
and im. Note that im returns a real of appropriate width,
rather than an imag.
Example.
Given a complex number
cwith the value3.14+2.72i, the expressionsc.reandc.imrefer to3.14and2.72respectively.
- proc complex.re ref
When used as a value, this returns the real component of the complex number as a real.
When used as an lvalue, this is a setter that assigns the real component.
- proc complex.im ref
When used as a value, this returns the imaginary component of the complex number as a real.
When used as an lvalue, this is a setter that assigns the imaginary component.
The standard Math module provides more functions on complex types.
See the Math module documentation.
The String Type
Strings are a primitive type designated by the symbol string
comprised of Unicode characters in UTF-8 encoding. Their length is
unbounded. Strings are defined in Strings.
The Bytes Type
Bytes is a primitive type designated by the symbol bytes comprised
of arbitrary bytes. Bytes are immutable in-place and their length is
unbounded. Bytes are defined in Bytes.
Enumerated Types
Enumerated types are declared with the following syntax:
enum-declaration-statement:
'enum' identifier { enum-constant-list }
enum-constant-list:
enum-constant
enum-constant , enum-constant-list[OPT]
enum-constant:
identifier init-part[OPT]
init-part:
= expression
The enumerated type can then be referenced by its name, as summarized by the following syntax:
enum-type:
identifier
An enumerated type defines a set of named constants that can be referred to via a member access on the enumerated type. Each enumerated type is a distinct type.
If the init-part is omitted for all of the named constants in an
enumerated type, the enumerated values are abstract and do not have
associated integer values. Any constant that has an init-part will
be associated with that integer value. Such constants must be parameter
values of integral type. Any constant that does not have an
init-part, yet which follows one that does, will be associated with
an integer value one greater than its predecessor. An enumerated type
whose first constant has an init-part is called concrete, since
all constants in the enum will have an associated integer value, whether
explicit or implicit. An enumerated type that specifies an init-part
for some constants, but not the first is called semi-concrete. Numeric
conversions are automatically supported for enumerated types which are
concrete or semi-concrete
(see Explicit Enumeration Conversions).
Example (enum-statesmen.chpl).
The code
enum statesman { Aristotle, Roosevelt, Churchill, Socrates }defines an abstract enumerated type with four constants. The function
proc quote(s: statesman) { select s { when statesman.Aristotle do writeln("All paid jobs absorb and degrade the mind."); when statesman.Roosevelt do writeln("Every reform movement has a lunatic fringe."); when statesman.Churchill do writeln("A joke is a very serious thing."); when statesman.Socrates do { write("I only wish that wisdom were the kind of thing that flowed "); writeln("... from the vessel that was full to the one that was empty."); } } }outputs a quote from the given statesman. Note that enumerated constants must be prefixed by the enumerated type name and a dot unless a use statement is employed (see The Use Statement and Using Modules).
It is possible to iterate over an enumerated type. The loop body will be invoked on each named constant in the enum. The following method is also available:
- proc enum.size : param int
Returns the number of constants in the given enumerated type.
- proc enum.first : enum
Returns the first constant in the enumerated type.
- proc enum.last : enum
Returns the last constant in the enumerated type.
Structured Types
The structured types are summarized by the following syntax:
structured-type:
class-type
record-type
union-type
tuple-type
Classes are discussed in Classes. Records are discussed in Records. Unions are discussed in Unions. Tuples are discussed in Tuples.
Class Types
A class can contain variables, constants, and methods.
Classes are defined in Classes. The class type can also contain type aliases and parameters. Such a class is generic and is defined in Generic Types.
A class type C has several variants:
CandC?owned Candowned C?shared Candshared C?borrowed Candborrowed C?unmanaged Candunmanaged C?
The variants with a question mark, such as owned C?, can store
nil (see Nilable Class Types). Variants without a
question mark cannot store nil. The keywords owned, shared,
borrowed, and unmanaged indicate the memory management strategy
used for the class. When none is specified, as with C or C?, the
class is considered to have generic memory management strategy.
See Class Types.
Record Types
Records can contain variables, constants, and methods. Unlike class types, records are values rather than references. Records are defined in Records.
Union Types
The union type defines a type that contains one of a set of variables. Like classes and records, unions may also define methods. Unions are defined in Unions.
Tuple Types
A tuple is a light-weight record that consists of one or more anonymous fields. If all the fields are of the same type, the tuple is homogeneous. Tuples are defined in Tuples.
Data Parallel Types
The data parallel types are summarized by the following syntax:
dataparallel-type:
range-type
domain-type
mapped-domain-type
array-type
index-type
Ranges and their index types are discussed in Ranges. Domains and their index types are discussed in Domains. Arrays are discussed in Arrays.
Range Types
A range defines an integral sequence of some integral type. Ranges are defined in Ranges.
Domain, Array, and Index Types
A domain defines a set of indices. An array defines a set of elements that correspond to the indices in its domain. A domain’s indices can be of any type. Domains, arrays, and their index types are defined in Domains and Arrays.
Synchronization Types
The synchronization types are summarized by the following syntax:
synchronization-type:
sync-type
atomic-type
The sync type is discussed in Synchronization Variables. The atomic type is discussed in Atomic Variables.
Type Aliases
Type aliases are declared with the following syntax:
type-alias-declaration-statement:
privacy-specifier[OPT] 'config'[OPT] 'type' type-alias-declaration-list ;
external-type-alias-declaration-statement
type-alias-declaration-list:
type-alias-declaration
type-alias-declaration , type-alias-declaration-list
type-alias-declaration:
identifier = type-expression
identifier
A type alias is a symbol that aliases the type specified in the
type-expression. A use of a type alias has the same meaning as using
the type specified by type-expression directly.
Type aliases defined at the module level are public by default. The
optional privacy-specifier keywords are provided to specify or
change this behavior. For more details on the visibility of symbols, see
Visibility Of A Module’s Symbols.
If the keyword config precedes the keyword type, the type alias
is called a configuration type alias. Configuration type aliases can be
set at compilation time via compilation flags or other
implementation-defined means. The type-expression in the program is
ignored if the type-alias is alternatively set.
If the keyword extern precedes the type keyword, the type alias
is external. The declared type name is used by Chapel for type
resolution, but no type alias is generated by the backend. See the
chapter on interoperability
(Interoperability) for more information on
external types.
The type-expression is optional in the definition of a class or
record. Such a type alias is called an unspecified type alias. Classes
and records that contain type aliases, specified or unspecified, are
generic (Type Aliases in Generic Types).
Example (type-alias.chpl).
The declaration
type t = int;defines a
tas a synonym for the typeint. Functions and methods available onintwill apply to variables declared with typet. For example,var x: t = 1; x += 1; writeln(x);will print out
2.
Querying the Type of an Expression
type-query-expression:
expression . 'type'
The type of a an expression can be queried with .type. This
functionality is particularly useful when doing generic programming
(see Generics).
Example (dot-type.chpl).
For example, this code uses
.typeto query the type of the variablexand store that in the type aliast:var x: int; type t = x.type;Open issue.
Given a nested expression that has
.typecalled on it, for examplef()inf().type, in which circumstances shouldf()be evaluated for side effects?At first it might seem that
f()should never be evaluated for side effects. However, it must be evaluated for side effects iff()returns an array or domain type, as these have a runtime component (see Types with Runtime Components). As a result, shouldf()in such a setting always be evaluated for side effects? The answer to this question also also connected to the question of whether or not a when a function returning atypeis evaluated for side effects at runtime.One approach might be to introduce different means to query only the compile-time component of the type or only the runtime component of the time.
Operations Available on Types
This section discusses how type expressions can be used. Type expressions
include types, type aliases, .type queries, and calls to functions
that use the type return intent.
A type expression can be used to indicate the type of a value, as with
var x: typeExpression; (see Variable Declarations).
A type expression can be passed to a type formal of a generic
function (see Formal Type Arguments).
The Types module provides many functions to query properties of
types.
The language provides isCoercible,
isSubtype, and
isProperSubtype for comparing types.
The normal comparison operators are also available to compare types:
==checks if two types are equivalent
!=checks if two types are different
It is possible to cast a type to a param string. This allows a type
to be printed out.
Example (type-to-string.chpl).
For example, this code casts the type
myTypeto a string in order to print it out:type myType = int; param str = myType:string; writeln(str);It produces the output:
int(64)Open issue.
If type comparison with
==is called on two types with runtime components (see Types with Runtime Components), should the runtime component be included in the comparison? Or, should==on types only consider if the compile-time components match?
Types with Runtime Components
Domain and array types include a runtime component. (See Domains and Arrays for more on arrays and domains).
For a domain type, the runtime component of the type is the distribution over which the domain was declared.
For an array type, the runtime component of the type contains the domain over which the array was declared and the runtime component of the array’s element type, if present.
As a result, an array or domain type will be represented and manipulated at runtime. In particular, a function that returns a type with a runtime component will be executed at runtime.
These features combine with the .type syntax to allow one to create
an array that has the same element type, shape, and distribution as an
existing array.
Example (same-domain-array.chpl).
The example below shows a function that accepts an array and then creates another array with the same element type, shape, and distribution:
proc makeAnotherArray(arr: []) { var newArray: arr.type; return newArray; }The above program is equivalent to this program:
proc equivalentAlternative(arr: []) { var newArray:[arr.domain] arr.eltType; return newArray; }Both create and return an array storing the same element type as the passed array.
Open issue.
Should a record or class type also have a runtime component when it contains array/domain field(s)? This runtime component is needed, for example, to create a default-initialized instance of such a type in the absence of user-defined default initializer.
Open issue.
Class types are not currently considered to have a runtime component. Should class types be considered to have a runtime component, so that querying an instance’s type with
myObject.typewill produce the type of the object known at runtime, rather than the type with whichmyObjectwas declared?Open issue.
Should functions returning a type always be evaluated for side effects, or only evaluated for side effects when returning a type with a runtime component?