Chapel Tutorial

Contents

  1. Beginning Chapel
    1. About this Tutorial
    2. About Chapel
    3. Installation and Configuration
    4. Hello World!
    5. Input and Output
    6. Variables
    7. Declaration Modifiers
    8. If-Else
    9. Arrays
    10. Range
    11. For Loops
    12. While Loops, Do-While Loops
    13. Records and Classes
  2. Parallelism in Chapel
    1. Forall and Coforall
    2. Begin and Cobegin
    3. Sync Variables
    4. Sync Statements
    5. Reduce
    6. Custom Reductions
    7. Scan
  3. Additional Topics
    1. Operators
    2. Range Operators
    3. Tuples
    4. Domains
    5. Select
    6. Procedures
    7. Modules

1. Beginning Chapel


1.1. About this Tutorial

This tutorial is designed to quickly get you started programming in Chapel. It is meant to serve as an introductory guide to Chapel. We begin with a discussion of the basic and serial parts of Chapel before moving on to the parallel aspects. It is written to be accessible to a broad audience so we do not assume background other than familiarity with a programming language such as Java or C. More advanced tutorials written by the Chapel group are available here. They have also written a "quick reference sheet" which summarizes Chapel syntax in a single page.

This tutorial is based on those, plus another tutorial written by Kyle Burke that is available here.

The authors of this tutorial are Johnathan Ebbers, Austin Finley, Maxwell Galloway-Carson, Michael Graf, Sung Joo Lee, Matt Lichty, Andrei Papancea, Casey Samoore. Its creation was partially supported by National Science Foundation's "Transforming Undergraduate Education in Science, Technology, Engineering and Mathematics (TUES)" program under Award No. DUE-1044299 and the Andrew W. Mellon Foundation. It is currently being maintained by David Bunde. Please let us know what you think of the tutorial so that we can continue to improve it.

1.2. About Chapel

Chapel is a parallel programming language developed by Cray. Its syntax is fairly similar to other imperative programming languages such as C or Java. It includes support for popular programming styles such as object-oriented programming and type-generic programming (think Java generics or C++ templates), but the main attraction is that it allows the programmer to easily express different kinds of parallelism in a portable way. It does this by giving the programmer both high-level abstractions such as reductions as well as supporting a low-level approach based on locks and threads. The high-level abstractions essentially automate common forms of parallel programming, allowing the programmer to be more productive and eliminating potential mistakes. Providing the low-level approach gives the programmer more control (if desired) and allows them to "get their hands dirty" when doing so is necessary for performance.

1.3. Installation and Configuration

Instructions for installing Chapel are here.

1.4. Hello World!

Let's begin with writing one of the simplest programs in any language "Hello World!", a program which prints this message to the screen. To write this program in Chapel, type the following line into a new file named hello.chpl:

writeln("Hello world!");
And that's it. To compile it from a terminal in the same directory, use the command
chpl -o hello hello.chpl
This invokes the Chapel compiler chpl, which translates hello.chpl into C and then uses a standard C compiler on it. The result is an executable called hello (No interpreter nor runtime environment is needed because the program has been translated all the way into machine code.) You can run it with:
./hello
If the result is not "Hello world!" then you should be concerned. Go back and make sure that Chapel is installed correctly.

1.5. Input and Output

Basic output in Chapel is rather simple. The two main procedures for output (Chapel's functions/methods) to use are write and writeln. The difference is that writeln will append a newline character to the end of all of the text output. These functions are similar to Java's System.out.print and System.out.println commands. Here's an example of a short program that prints a line of text containing two variables:

var x: int = 7;
var y: int = -13;
writeln ( "The variable x is equal to ", x, ", and the variable y is equal to ", y, "." );
The output is:
The variable x is equal to 7, and the variable y is equal to -13.
The equivalent in C and Java, respectively:
// both
int x = 7;
int y = -13;

// c
printf ( "The variable x is equal to %d, and the variable y is equal to %d.\n", x, y );

// Java
System.out.println( "The variable x is equal to " + x + ", and the variable y is equal to " + y + "." );
To read input in a Chapel program, one makes a read or readln procedure call attached to a file object. stdin is a built-in file structure representing standard input, which comes in through the command line (including use of pipe and filter). read and readln are passed the types of input they should return (e.g. int or string), and return those types. If line has been declared as a string variable (which are like Java's String, except they are primitives in Chapel), to read from the user or some piped in data, one could make a call such as:
var line: string = stdin.readln(string);
writeln("You typed: ",line);
This will take a line from standard input and place it into the string variable, line. One can also read in types such as numbers, as well. (note that, depending on your installation, readln may not take the whole line, but just the first string delimited by whitespace.)
var num: int;
num = stdin.read(int);
writeln("You typed: ",num);
The code above reads the next int from stdin and stores it in the variable num. read and readln, of course, can also be nested within loops and other structures. When used within a loop, a built-in variable, eof, is set to true by the system when stdin reaches the end of input. If stdin can no longer read input, but attempts to, an error will be thrown and the program will exit.

Exercise

Practice some I/O by writing a Chapel program that reads a word and prints it right back out.

var word: string;
word = stdin.read(string);
writeln(word);

1.6. Variables

Chapel supports several simple variable types and a somewhat different way of defining and initializing them. For this introduction, there are four basic types that you should know:

bool

A simple boolean variable, holding the value either true or false. The default value for bool is false.

int
Integer variable as in Java and C. Currently, declare a long with int(64) as the type, but 64 bits has been the default int size starting with Chapel version 1.5. If you need it unsigned, declare it as type "uint". The default value for int is 0.

real
Floating point variable. This is equivalent to a double in Java or C. The default value for real is 0.0.

string
Variable to hold strings of ascii characters; essentially identical to strings in Java, except that these are not objects, but primitives. String concatenation works the same in Chapel as it does in Java. The default value for string is "" (the empty string).

1.7. Declaration Modifiers

var: Required to declare an ordinary variable that can be altered after initialization.

const: Used to declare a variable that is constant -- that cannot be altered after initialization.

config: Config variables must be declared (and initialized) at the beginning of the module (similar to Java's class, and will be discussed later on) containing the main method, outside of any methods themselves. Defining a variable as a config essentially allows you to change the default value of the variable at runtime when calling your chapel program via the command line – for example

./configExample --b=false --i=413 --r=2.7 --s="I like Chapel!"
Assuming configExample has config variables b, i, r and s defined, whereas they refer to variables of types boolean, integer, real and string, respectively, calling configExample on the command line in this fashion will cause configExample to initialize these variables to the values given. This is not only useful, but is currently the primary method of passing arguments to the main methods of Chapel programs. Note that the config modifier must be used with either the var or const modifiers; not alone, and obviously not with both together (as var and const are essentially opposites). Check out the sample code below:
config var b: bool = true;
config var i: int = 1;
config var r: real = 3.14;
config var s: string = "Hello world!";
writeln("boolean: ", b, ", int: ", i, ", real: ", r, ", string: ", s);
Note that in the above example, if the program is called without defining any config variables, the defaults set in the code above will be used, and the output will be:
boolean: true, int: 1, real: 3.14, string: Hello world!
Also note that the config variables do not have to be pre-initialized, as they are in the above example. If they are not, and the program is not called with config options setting values for them, the values will be set to the respective default values of their variable types (i.e. 0 for int, false for bool, 0.0 for real, and "" for string).

Now, try changing the config variables using the command line example above.

Four ways to define a variable:
  1. [config] var/const identifier = value;
  2. [config] var/const identifier: type;
  3. [config] var/const identifier: type = value;
  4. [config] var/const identifier: type = value:type;
Note that config is always optional and, in fact, can only be used within module scope but outside of method scope. Using either var or const is mandatory, as well as an identifier. One can technically define a variable without mentioning type, as in the first example, but this not a recommended practice, and will not be used in any of our examples. The second example of variable definition does not initialize the variable. The third and fourth examples do initialize the variable, the difference being that the latter casts the given value to the defined type. Casting will be discussed more later on.

1.8. If-Else

Conditionals in Chapel (i.e. if-else statements) have a very similar syntax to the ones in C or Java. The only difference is that you need to use the then keyword if you omit braces:

if(condition) {
doStuff();
} else {
doOtherStuff();
}

if(condition) then
doStuff();

As in most programming languages, the else statement is optional.

1.9. Arrays

Creating arrays in Chapel is quite different from that of C or Java. The two examples below demonstrate it:

var A: [1..3] int = (5, 3, 9);
var B: [1..3, 1..5] real;
In each of the examples, var is used to indicate the arrays as modifiable variables, rather than constants. A and B are the names of the arrays. In this case, the semi-colon (:) is not used as a cast operator. In the example A, an array of 3 integers is created (with the indices 1, 2, and 3), and they are assigned the values 5, 3, and 9, respectively. In example B, a two dimensional array of size 3 by 5 is created, but the values are not initialized.

1.10. Range

A range expression is a unique expression to Chapel that takes the form of [low]..[high], and represents a sequence of numbers. 1..4 represents 1, 2, 3, and 4, for example. Note that if the first number in the range is greater than the second (e.g. 6..2), the range is empty. If low or high is unspecified, then the range is unbounded in that direction (e.g. the range, 5.. results in a range of numbers starting with 5, and continuing on for infinity. A range with an infinite bound, however, cannot be used in as a conditional, such as in a for loop, unless it is intersected with at least one other range. An example of a range intersection is:

(1..6)(3..) // which yields 3, 4, 5, 6

1.11. For Loops

In Chapel for loops have the following syntax:

for index-expression in iterable-expression {
//statements
}
The "iterable-expression" can either be a predefined collection such as an array or a tuple, or a range generated on the fly. Here's an example:
var length:int = 5;
var A: [1..length] string = ( "sudo", "make", "me", "a", "sandwich!" );

//This iterates through the numbers 1, 2, 3
for i in [1..3] {
write(i," ");
}

//This has the functionality of a foreach loop in Java
for a in A {
write(a," ");
}
Note that, opposed to Java and C, you do not need to declare the type of the variable used for iteration (ex. i or a above).

If the loop body is a single statement, you can omit the parentheses by following the iterable expression with the keyword do as in the following:

for index-expression in iterable-expression do
//single statement

Exercise

Write a program that stores the powers of 2 from 1 to 2n in an array and then iterate through the array to print its elements from smallest to largest. The variable n should be a config variable with a default value of 10.

config var n: int = 10;
var A: [0..n] int;
var x: int = 1;

//Notice how "a" is an element of the array A and that you can store values in the array by assigning them to "a"
for a in A {
    x *= 2;
    a = x;
}

for i in 0..n {
    writeln(A(i));
}

1.12. While Loops, Do-While Loops

While loops and do-while loops share the same syntax as in C or Java. Check out the example below:

while (condition) {
//work
}
The example below is a do-while loop and the main syntactical difference between it and a regular while loop is that the condition is checked at the end instead of the beginning. Furthermore, there must be a semicolon after the while statement.
do {
//work
} while (condition);

1.13. Records and Classes

Chapel's implementation of structures comes in two flavors: records and classes. The distinction is that records are value-based and classes are reference-based – in other words, an assignment of a record literally copies the record (much like an assignment of a structure in C), whereas an assignment of a class copies a reference to the class object (as in pointers to structures in C, or object references in Java). An example of a record is:

record circle {
var r: real;
proc area() { //defines a method within a record
return 3.14 * ( r ** 2 );
}
}
Once the record circle is defined you can perform operations on with it like shown below:
var c1, c2: circle; //'circle' is the type
c1 = new circle(12.0); //created using the 'new' keyword, as in Java and C++
c2 = c1; //literally copies the data from c1 to c2
c2.r = 10;

writeln("The area of c1 is ",c1.area()," and the area of c2 is ",c2.area());
Note that after copying c1 to c2, we changed the radius of c2 whithout affecting c1.

The syntax of a class is nearly identical to the one of a record, with the exception that the class keyword is used instead of the record keyword. Furthermore, c1 and c2, after the assignment, would not be variables that contain the data of the circle object, but rather they would simply both have an identical reference/pointer to the same circle object, stored somewhere in memory. To understand better here's the circle example above, changed from record to class:
class circle {
var r: real;
proc area() { //defines a method within a record
return 3.14 * ( r ** 2 );
}
}
Once the class circle is defined you can perform operations on with it like shown below:
var c1, c2: circle; //'circle' is the type
c1 = new circle(12.0); //allocated using the 'new' keyword, as in Java and C++
c2 = c1; //makes c2 another reference to the same circle as c1 c2.r = 10;//changes the radius r for both c1 and c2 because they point to the same data

writeln("The area of c1 is ",c1.area()," and the area of c2 is ",c2.area());//they will be equal
You should notice that the area of c1 and c2 is the same, because c1 and c2 refer to the same object.

Additionally, unlike for records, Chapel does not automatically deallocate the memory used by a member of a class when no longer in use. When you work with class objects, you need to use the delete keyword to clear up the memory taken up by the objects that are no longer used. In the example above, you would delete the shared memory referenced by c1 and c2 like this:
delete c1;


2. Parallelism in Chapel

Now that you have a better grasp of Chapel you should be ready to learn the parallel features of it, permitting you to write scalable parallel programs. Chapel's main forms of parallelization are done via tasks and threads. The most common methods of parellelizing code in Chapel are to use a combination of forall and coforall loops, and the begin and cobegin statements. Let's take a look!

2.1. Forall and Coforall

Forall loops are a simple and easy way to run similar tasks in parallel using a for loop. When a forall loop runs, it creates a thread for each core of the processor. After the threads have finished they are joined together to form the result. Note that threads are created at the beginning of the first iteration through the loop.

var sum : int = 0;
forall i in 1..1000000 {
sum += i; //DANGER: race condition!
}
writeln(sum);
Try running the above code. You should notice that the program will output a different result almost every time. This is due to a race condition that forall creates. Basically, multiple threads attempt to change the value of sum at the same time. To solve this problem you need to use sync variables, discussed below.

Despite the fact that the iterations of the for loop do not necessarily need to run in order, forall is a very easy way to parallelize the loop. Note that parallelizing any code comes with some overhead, and that if too many threads are created (e.g. parallel code called within parallel code creates more threads that branch off of their parent threads), or that if a substantial amount of computation is not being parallelized at once, the parallel code may very well run slower than the code in serial. Also remember that there are certain serial constraints of our modern computers – e.g. retrieving or writing data from or to a hard disk is a serial operation that cannot be parallelized.

Coforall loops work in a similar way to forall loops. However, in a coforall loop, a new thread is created at each iteration through the loop. This is useful in cases in which each iteration has a substantial amount of work, and the number of tasks should be equal to the number of iterations. Check out the example below:
// here.numCores returns the number of cores your processor has

config const numTasks = here.numCores;
coforall tid in 1..numTasks {
writeln("Hello, world"," from task ",tid," of ",numTasks,"!");
}

2.2. Begin and Cobegin

The other forms of asynchronous parallelization in Chapel are the begin and cobegin statements. By using a begin statement on a process you create a different thread for each statement.

begin writeln("I'm one thread");
begin writeln("I'm another thread");
begin writeln("I'm yet another thread");
begin writeln("I am less important and can wait");
By running this program multiple times you will see that order in which these are printed out to the terminal changes. This shows that the statements run asynchroniously.

cobegin statements are different in that the calling code waits for the cobegin's block of parallelized code to finish before continuing. Let's take a look at the example below:
cobegin {
writeln("I'm one thread");
writeln("I'm another thread");
writeln("I'm yet another thread");
}
begin writeln("I am less important and can wait");
Essentially, the cobegin example above is almost equivalent to the begin example because all the writeln statements run asynchroniously, yet the main difference is that no code can run until the cobegin block has finished. Thus, the writeln statement outside the cobegin block will always run last.

2.3. Sync Variables

To prevent race conditions on variable updates, you can use sync variables. These variables can store a value as normal, but they also have two states: empty and full. When a sync variable is full it will stop any other thread to use it until it is turned empty. A sync variable is set to full when it is given a value and it is set to empty whenever it is assigned to another sync variable.

For example, we can fix the broken forall loop in section 2.1 by making the sum into a sync variable:

var sum : sync int = 0;
forall i in 1..1000000 {
sum += i;
}
writeln(sum);
Now, the first thread to read sum empties it, forcing other threads to wait until the new value is written.

Sync variables can also be used as a lock in the style of other languages. For example, here is another way to fix this summation example:

var lock: sync bool; //the sync variable
var sum: int = 0;
forall i in 1..1000000 {
lock = true; //the sync variable is set to full
sum += i;
var unlock = lock; //empty the variable allowing the next process in
}
writeln(sum);
In this example the empty/full state of lock is used to indicate whether the lock is available or held. Note that in the above example, unlock is just an arbitrary variable name. It is just used to read the value from lock, causing that variable revert to the empty state.

2.4. Sync Statements

sync can also be applied to a statement or block of code. When used this way, it will join together all begin statements started within that statement or code block. This can prevent potential race conditions that would occur if the program did not wait for begin statements to finish. It can be used to create a construct similar to cobegin, as the following segnment of code demonstrates:

sync {
begin writeln("I'm one thread");
begin writeln("I'm another thread");
begin writeln("I'm yet another thread");
}
begin writeln("I am less important and can wait");
This code will act very similarly to:
cobegin {
writeln("I'm one thread");
writeln("I'm another thread");
writeln("I'm yet another thread");
}
begin writeln("I am less important and can wait");
However, sync statements and cobegin statements differ in how they treat nested begin statements. For example, consider the following two procedures:
proc statement1() {
begin writeln("I'm one thread");
begin writeln("I'm another thread");
}

proc statement2() {
begin writeln("I'm yet another thread");
}
Launching these procedures with a begin inside a sync block will make the code wait for the tasks launched inside statement1 and statement2 to finish:
sync {
begin statement1();
begin statement2();
}
begin writeln("I am less important and can wait");
Calling those procedures with a cobegin will not force the program to wait for the tasks launched inside statement1 and statement2.
cobegin {
statement1();
statement2();
}
begin writeln("Maybe I am just as important now.");
You can also use sync statements with loops. The following code computes the value of pi by adding up the area of many rectangles under half of a circle and doubling that value.
const numRect = 10000000;
const width = 2.0 / numRect; // rectangle width
const numThreads = here.numCores; // number of cores the computers processor has
var globalSum: real = 0.0;

proc calculateArea(init) {
var partialSum: real = 0.0;
var x: real;
var i: int = init;
do {
x = -1 + ( i + 0.5) * width;
partialSum += sqrt(1.0 - x*x) * width;
i += numThreads;
} while (i < numRect-1);
globalSum += partialSum;
writeln("Thread: ", init, " globalSum: ", globalSum);
}

for i in 1..numThreads {
begin calculateArea(i);
}

writeln("This code estimates pi as ", globalSum*2);
If you run this code, you may notice a race condition; the code might print the value of globalSum*2 before the tasks launched with begin all add their partial sum to the globalSum. (Due to the nature of race conditions, this doesn't always occur.) You can fix this race condition by adding a sync statement to the for loop that creates the tasks:
sync for i in 1..numThreads { ... }

2.5. Reduce

Reduce is an operator that combines a set of values to produce a single value. Reduce is useful because in parallel computation it is almost always necessary at some point to compare or combine results produced by different threads. The syntax for reduce is:

var varName = reduce_operator reduce iterator_expression;
In the code above, valid reduce_operators are: +, *, &, |, ^, &&, ||, min, max, minloc, and maxloc. Furthermore, iterator_expression can be an expression of any type that can be iterated over, provided the reduction operator can be applied to the values yielded by the iteration. For example, the bitwise-and operator can be applied to arrays of boolean or integral types to compute the bitwise-and of all the values.

To sum up all the elements of an array A of size 10, you write:
var sum = + reduce A;
The code above is equivalent to the following:
var sum = + reduce ([i in D] A[i]);
Basically, [i in D] A[i] iterates through all the elements of A, where D is a domain from 1 to 10 (see section 3.4). Notice how you can use a loop as the iterable expression.

The code below computes the value of PI with a precision of 5 digits. Run it, see if it works and focus mostly on the for loop.
const numRect = 10000000;
const D : domain(1) = 1..numRect;
const width = 2.0 / numRect; //rectangle width
const baseX = -1 - width/2; //baseX+width is midpoint of 1st rectangle

proc rectangleArea(i : int) { //computes area of rectangle i
const x = baseX + i*width;
return width * sqrt(1.0 - x*x);
}

var halfPI : real;

for i in D {
halfPI += rectangleArea(i);
}

writeln("Result: ",2*halfPI);
In the above example replace the (serial) for loop with the following:
halfPI = + reduce rectangleArea(D);
Run the code again and see that it works. Besides the fact that the code is shorter, it is also faster. To time the code use the Time module (see section 3.7).

Exercise

Use reduce to compute the minimum and the second minimum of the following array:

var D: domain(1) = (1..10);
var A: [D] int = (-5,6,-2012,-75,2012,48,-700,65,100,0);
Remember that you can use any iterable expression after the reduce keyword (i.e. loops).

var D: domain(1) = 1..10;
var A: [D] int = (-5,6,-2012,-75,2012,48,-700,65,100,0);

var minVal = min reduce A;
var secMin = min reduce ([i in D] if A[i] > minVal then A[i]);

writeln("The minimum is ",minVal," and the second minimum is ",secMin,".");

2.6. Custom Reductions

A custom reduction is powerful tool that allows you to reduce an iterable expression with a custom operator other than the default ones (see above). To build a custom operator you need to write a class that has the following structure:

class myOperator : ReduceScanOp {
type eltType;
var myVar : eltType;

proc accumulate(val :eltType){
//do what the operator should do
}

proc combine(other : myOperator){
//do what the operator should do (again)
}

proc generate(){
return myVar;
}
}
So, let's take a look at the code above. First of all, you probably noticed the eltType keyword. This is basically a generic type, that inherits the type of the elements of the iterable expression you are reducing. For instance, if you were to do a custom operation on an array of integers, then when you the reduce runs, eltType will become an int. Furthermore, the accumulate procedure takes the value parsed from a certain iteration of the expression and it applies the custom operation to it.

Because reduce runs the operation in parallel, each thread of execution will deal with a small part of the iterable expression. Thus, the threads, in pairs, need to communicate with each other at some point to produce the global result. That's where you use the combine procedure which should contain almost the same code as the accumulate procedure (we will see this a bit later).

Finally, after all threads have joined their results, the global result is returned by the generate procedure.

As you might expect, you run a custom reduction, just like you would run a regular one. So, for the example above you would write:
var someVar : type = myOperator reduce iterable_expression;
To give you a better example of custom reductions and their power, here's an alternate solution to the practice exercise in section 2.4. The code below uses custom reductions to return the second minimum in the array A:

var A: [1..10] int = (-5, 6, -2012, -75, 2012, 48, -700, 65, 100, 0);

class customMin : ReduceScanOp {
type eltType;
var min : eltType = max(eltType);
var secMin : eltType = 0;

proc accumulate(val : eltType) { //accumulate val into result
if(val < min) {
secMin = min;
min = val;
} else if(val <= secMin){
secMin = val;
}
}

proc combine(other : customMin) { //combine 2 partial reductions
if(other.min < min) {
secMin = min;
min = other.min;
} else if(other.min <= secMin){
secMin = other.min;
}
}

proc generate() { //return the current state
return secMin;
}
}

writeln("The second minimum is ",customMin reduce A,".");

2.7. Scan

A scan embodies the logic that performs a sequential operation in parts and carries along the intermediate results. Loop iterations appear to be sequential because they accumulate information in order as they iterate, but on closer inspection, they can often be solved using a scan, which admits more parallelism. The syntax for scan is:

var varName = scan_operator scan iterator_expression;
The scan operators are the same with the reduce operators, and the same applies to the iterator_expression - it can be any iterable expression.

A simple example using scans is:
var A: [1..5] int = 1; //creates an array with 5 elements, each initialized to 1
writeln(+ scan A);
The output of the code above is:
1 2 3 4 5
Note that the main difference between scans and reductions is that a scan returns the intermediate results up to, and including the final result, whereas the output of a reduction is only the final result.


3. Additional Topics

3.1. Operators

Operators in Chapel are virtually identical to those in C or Java. There are, however, a couple of exceptions:

cast:

The cast operator, or ":", is used to cast a variable of one type to another type. This is generally done in the form, operand:type, where the operand could be either a variable or a literal. For example, to cast a string to an int one would write:
varName:int;

exponentiation:

The exponentiation operator, **, is used to raise a number left of the operator to the power of the right operator. For example,
writeln("1 kilobyte has ",2**10," bytes");
Note that this only works with int and real variables of default bitsize, and does not accomodate unsigned integers.

exponentiation:

The swap operator, <=>, is used to swap the values of the variable to the left with that of the variable to the right. Note that the variables' types must match:
var a:string ="A";
var b:string = "B";
writeln("a is ",a," and b is ",b);
a <=> b;
writeln("a is ",a," and b is ",b);

// The output will be:
// a is A and b is B
// a is B and b is A

3.2. Range Operators

Within the context of arrays, it is important to introduce one more thing about ranges. In order to allow flexibility within control structures such as for loops, Chapel has range operators, which allow you to create patterns for iterating through the range. For example:

module sandwichMaking {
config var userHasPermissions: bool = false;
proc main() {
var B: [1..12] string = ( "System ", "sandwich. \n", "does ", "a ", "not ", "you ",
  "comply. ", "Making ", "No ", "complies. ", "sandwiches. \n", "System " );
var t: int = 2;
if(userHasPermissions) then t *= -1;
for b in 1..12 by t {
write(B(b));
}
}
}
This program outputs the following, if userHasPermission is set to false:
System does not comply. No sandwiches.
If userHasPermission is true then the output is:
System complies. Making you a sandwich.
Note that when the range operator is negative it will iterate through the list backwards.

The important point to note here is that in the for loop construction the keyword by is used after the range to iterate t times.

Another range operator is the pound "#" character. Check out the example below:
for i in 1..100 # 5 {
write(i," ");
}
The code above will print the first five numbers in the range (1 through 5).

Additionally, a negative number can be provided to the "#" range operator. Using # -5 in the above code would cause it to print the last five numbers (96 through 100), in increasing order. Note that range is inclusive, and so the first number printed will be 96, and the last will be 100. Because we sometimes want a countdown in a for loop, the "#" and by operators can be combined to output the last five elements of the range in decreasing order:
for i in 1..100 # -5 by -1 {
write(i," ");
}

Exercise

Write a Chapel program that prints out every other number from 1 to 10, and then prints out every number from 1 to 10 in descending order. Separate the numbers by a comma.

for i in 1..10 by 2 {
    write(i,", ");
}

writeln();

for i in 1..10 by -1 {
    write(i,", ");
}

3.3. Tuples

Tuples are variables that contain groups of numbers, such as ordered pairs. This is best explained by the following example:

var tuple1: (int, int) = (7, 20);
var tuple2: (int, int) = (x, y);
var tuple3: (int, string, real) = (7, "Chapel",12.0);
In the example above, tuple1 contains two int values, tuple2 contains two int variables, and tuple3 contains three elements that are all different types. To access the a member of a tuple use tupleName(i), where i is the index of the element you are trying to access. For instances tuple1(1) will return 7. Note that in Chapel, unlike in Java or C, the numbering of a data structure starts at 1 and not at 0.

Passing a tuple as an argument to a writeln call will print out the tuple's elements in order, separated by commas.

3.4. Domains

A domain is a range of indicies that can be dynamically resized. The example below shows you how to create a domain with a range of 1 to some int variable s:

var s: int = 5;
var D: domain(1) = 1..s;
var E: domain(2) = 1..s,1..s; //multiple dimensions are supported
To create an empty array A with the size of domain D write:
var A: [D] int;
You can resize the array defined by the construction of domain D like:
D = 1..s*2; // effectively doubles the size of the array defined by domain D, preserving its contents prior to the resize
E = 1..s*2, 1..s*2; // also works for a two-dimensional array

Exercise

Create a domain with the range from 1 to 10. Then define an array of integers with this domain and fill the array with the numbers 1 through 10. Now double the domain's size, which will also resize the array, yet retain its elements. Fill the rest of the array with the numbers 1 through 10 in descending order without writing over the original elements in the array. Finally, print out all the elements of the array.

var s: int = 10;
var D: domain(1) = 1..s;
var A: [D] int;

for i in 1..s {
    A[i] = i;
}

writeln(A);

D = 1..s*2;

for i in 1..s {
    A[i+10] = 11-i;
}

writeln(A);

3.5. Select

Select works similar to a switch statement in C or Java. The only difference is the wording: select instead of switch, when instead of case, and otherwise instead of default. The following is an example of a select statement in its proper Chapel syntax:

var x: int;
select ( x ) {
when 1 { //ie. if x is 1
//work
}
when 2 {
//work
}
otherwise {
//work
}
}

Exercise

Create a config variable and initialize it using the command line. Use a select statement to print "You chose 1.", "You chose 2.", or "You did not chose 1 or 2." depending on the inputed value.

config var x:int;

select(x){
    when 1 {
        writeln("You chose 1.");
    }
    when 2 {
        writeln("You chose 2.");
    }
    otherwise {
        writeln("You did not chose 1 or 2.");
    }
}

3.6. Procedures

Procedures are Chapel's version of functions/methods. Procedures are declared with the keyword proc, followed by the name of the procedure. Some key things to note about Chapel's procedures are that argument and return types can be omitted, allowing for generic programming. Also, procedure parameters can have default values specified so that the procedure can be called without passing any arguments. The scope of this tutorial does not cover omission of argument types, which is known as generic programming. Example of syntax for procedures that do not use arguments:

proc say_hello(){
var name: string = getName();
writeln("Hello, ",name,"!");
}

proc getName(): string{
writeln("What's your name?");
return stdin.read(string);
}

proc main(){
say_hello();
}
As you would expect, the program will first output "What's your name?". After you input your name the program will output "Hello, your_name!".

Note that the last procedure, main is Chapel's standard main method call. Unlike C and Java, however, Chapel's main method cannot take any arguments. Rather, information to the main method in Chapel is passed via config variables.

Also unlike C and Java, the use of the main procedure is not actually necessary. In C and Java, main is what runs when the file is compiled and executed. In Chapel, any instructions placed outside of a procedure will run automatically; furthermore, these instructions will actually have prescedence over main.

The the examples below illustrate how to pass arguments to procedures:
proc area(r: real): real {
return 3.14 * (r ** 2);
}

proc writeMultiple(text: string) {
for i in 1..5 {
writeln(text);
}
}

proc addTuple(a: int = 1, b: int = 2): int {
return a + b;
}
In the first example, area(r) returns the area of a circle with radius r. The second example takes a string as an argument and prints it 5 times, but doesn't return anything. This is analogous to a Java void function. The interesting component of the third example, addTuple(a,b), is that it has built-in default values, such that if addTuple(a,b) is called without any arguments (like this addTuple()), the default values for a and b will be 1 and 2, respectively, returning a value of 3.

3.7. Modules

A module is a construct that works much like a package does in Java. To access the procedures and variables in another class, you need to include its code at the beginning of the module with the keyword use, followed by the name of the module to use. If the module is not a Chapel standard or included in the same directory, a path to that module file will have to be specified (enclosed in quotes).

Chapel contains some library modules to make programming easier, much in the same way that Java and C have libraries. One obvious example is the Math module. This module does not need to be declared because it is a default module in all versions of Chapel. A complete listing of the procedures can be found in the language specification, available from the language definition part of the official site. Some helpful procedures are:

writeln(abs(-5.5));
writeln(atan(1));
writeln(round(5.5));
Another Chapel module is Time, which is used for timing operations. It is not a default module you need to declare it using "use Time;" in order to use its procedures. Here's an example of how you would use it:
use Time;

var t:Timer;

t.start();
var sum:int = 0;
for i in [1..10000000] {
sum += i;
}
t.stop();

writeln(t.elapsed()," seconds elapsed");