This tutorial is designed to quickly get you started programming in Chapel, and is meant to serve as an introductory guide to the language. 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 help and recources from others including Kyle Burke, Brad Chamberlain, Rahul Ghangas, and the Chapel development team.
The authors of this tutorial are Tegan Doherty, 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, The Baker-Velde Student Award Research Award, 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.
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:
use IO;
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:
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:
[config] var/const identifier = value;
[config] var/const identifier: type;
[config] var/const identifier: type = value;
[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.
To facilitate array operations, Chapel automatically promotes standard
scalar operators and functions to perform element-wise operations on arrays of
numbers.
A = 1; //sets all elements of A to 1
A = B; //copies elements of B into A
A = A + 1; //adds 1 to each element of array A
A = A * 2; //multiplies each element by 2
A = f(A); //applies function f to each element; works for library and user-defined functions
Note that writeln prints the entire array on one line rather
than printing each element on a separate line.
The given behavior is not really promoting the scalar operation, but
is the more commonly desired behavior in this case.
Also note that all these operations are potentially performed in parallel.
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, as 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 recordcircle is defined you can perform operations on with it like shown below:
var c2: circle; //'circle' is the type
var 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 without affecting c1.
The syntax of a class is nearly identical to that of a record, but unlike records, classes have different ways of managing memory: owned, borrowed, shared, and unmanaged. The difference between these strategies is in the ways the memory is freed. There are also differences in referencing, copying, and transferring them.
class circle {
var r: real;
proc area() { //defines a method within a record
return 3.14 * ( r ** 2 );
}
}
Once the classcircle is defined you can perform operations with it as shown below:
var c2: circle; //'circle' is the type
var c1 = new unmanaged 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 unless the owned keyword is used. When
you work with unmanagedclass 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;
To have the memory deallocated automatically when no longer in use you
must use the owned keyword. This also means that only one variable
can refer to that instance at a time, so when transfering ownership you must
use .borrow() instead of setting c2 equal to c1.
var c2: circle; //'circle' is the type
var c1 = new owned circle(12.0); //allocated using the 'new' keyword, as in Java and C++
c2 = c1.borrow(); //makes c2 another reference to the same circle as c1
c2.r = 10;//changes the radius r for c2 only because there can only be one at a time
writeln("The area of c2 is ",c2.area(),");//trying to get the area of both will cause an error because c1 is nill right now
To learn more about different strategies for managing memory you can look over this site: here
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 with (ref sum) {
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.The reason that "with (ref sum)" needs to be included is because Chapel will cause an error message in cases with obvious race conditions. The variable sum would be treated like it was constant, which would lead to an error when trying to change it.
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.numPUs() returns the number of cores your processor has
config const numTasks = here.numPUs();
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 and Atomic 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.readFF()); //readFF() only reads the variable when full and does not change the state
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, though it is much slower.
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.writeEF(true); //the sync variable is set to full
sum += i;
var unlock = lock.readFE();; //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, though that doesn't have to be the case.
The read and write methods for sync variables are customizable, which allows
for a bit more freedom when using them. The letters after the read or write call
determine when the operation can take place, and what happens to the variable
after. FE blocks until it's full and sets it to empty after. FF blocks until full
and leaves it full. EF blocks until empty and leaves it full. Go
here for more information.
Another way to deal with race conditions is to use atomic variables
In order to use atomic operations, a variable needs to be declared as atomic.
This is another way to synchronize tasks because only one task is able to
successfully write to an atomic variable at a time. This works because the
operations are run seperately from anything else. Some of the methods for
atomic variables are, add(), write(), read(), and waitfor().
var count: atomic int;
const tasks = here.numPUs();
count.write(0); //write sets the value
coforall i in 1..tasks {
writeln("this is task ", i, " waiting for all tasks");
count.add(1); //add increases the variable by the value in ()
count.waitfor(tasks); //waits for count to equal tasks-will not proceed until then
writeln("task ", i, " is done");
}
//read() returns the stored value
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 = 6; // put the number of cores your computers processor has
var globalSum: real = 0.0;
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_operatorreduceiterator_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;
//identity for whatever operation is being done
proc identity {
//return something that would not change the state
}
//add a single element onto accumulator
proc accumulate(val){
//do what the operator should do
}
//add an element to the state
proc accumulateOntoState(ref state, val){
//do what the operator should do but onto the state
}
//combine two accumulations
proc combine(other : borrowed ReduceScanOp){
//do what the operator should do (again)
}
proc generate(){
return myVar;
}
//produce new instance of class
proc clone() return new unmanaged ReduceScanOp(eltType=eltType);
}
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 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. accumulateOntoState does the same, but onto the given state instead of the accumulator. What goes in the identity procedure is whatever would result in no change to the state. So for a plus reduce operator this would be adding 0.
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 and a new instance of the class is made by the clone procedure.
For an example, here is a custom reduction for the plus reduce operator (at the bottom of the page).
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 minimum : eltType = max(eltType);
var secMin : eltType = 0;
proc identity return max(eltType);
proc accumulate(val : eltType) { //accumulate val into result
proc clone() return new unmanaged customMin(eltType=eltType);
}
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_operatorscaniterator_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 ",
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 provided by this module can be found
here
Some helpful procedures are:
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");
A list of all other standard modules in Chapel can be found
here
and the list of package modules can be found
here.