Procedures¶
View procedures.chpl on GitHub
This primer covers procedures including overloading, argument intents and dynamic dispatch.
A procedure groups computations that can be called from another part of the program. The procedure can be defined with zero or more “formal” arguments. Each formal argument can have a default value associated with it.
Formal arguments are supplied with values when the procedure is called.
The arguments supplied at the call site are the “actual” arguments.
If a name and =
precede an actual argument, the actual is assigned
to the formal argument with that name. Any remaining (unnamed) actual
arguments are assigned to the remaining formal arguments in lexical order.
A procedure can return zero, one or more values (as a tuple). The return value type can be specified after the formal argument list. If no explicit return value type is supplied, the Chapel compiler infers the return value type.
Here is a procedure which takes an integer argument and returns an integer result. It computes the factorial of the argument.
proc factorial(x: int) : int
{
if x < 0 then
halt("factorial -- Sorry, this is not the gamma procedure!");
return if x == 0 then 1 else x * factorial(x-1);
}
writeln("A simple procedure");
writeln("6! is ", factorial(6));
writeln();
Overloading Functions¶
Default integers in Chapel are 64-bits, so we may want to specify a version of factorial that operates on 32-bit integers to save space and potentially time (depending on the target architecture). This version also optimizes a bit, compressing the callstack by a factor of two by doing two multiplies.
This version “overloads” the previous version of factorial.
Upon a call to factorial()
, the compiler will choose the best fit.
proc factorial(x: int(32)) : int(32)
{
if x < 1 then
halt("factorial -- Invalid operand.");
if x < 3 then return x;
return x * (x-1) * factorial(x-2);
}
The argument type of this version must be different, so the two versions of factorial can be differentiated. If we pass in a (default) 64-bit integer value, we will get the 64-bit version.
writeln("Another simple procedure");
writeln("33! is ", factorial(33));
Whereas passing in a 32-bit integer will cause us to get the 32-bit version:
writeln("6! is ", factorial(6:int(32)));
writeln();
Overloading Operators¶
Procedure definitions allow you to overload operators, too. Here
we define a new type, Point
, and overload the definition of +
to handle that type.
record Point : writeSerializable { var x, y: real; }
Tell how to add two points together.
operator Point.+(p1: Point, p2: Point)
{
// Vector addition in 2-space.
return new Point(p1.x + p2.x, p1.y + p2.y);
}
We can also overload the serialize
routine called by writeln.
proc Point.serialize(writer, ref serializer) throws
{
// Writes it out as a coordinate pair.
writer.write("(");
writer.write(this.x);
writer.write(", ");
writer.write(this.y);
writer.write(")");
}
writeln("Using operator overloading");
var down = new Point(10.0, 0.0);
var over = new Point(0.0, -5.0);
writeln("down + over = ", down + over);
writeln();
Details on Arguments¶
Here we define a class, Circle
, and a function which creates a
specific instance of it using a different style of argument
definition than we have previously encountered.
class Circle {
var center : Point;
var radius : real;
}
Note that a default value for an argument can be provided,
which will be used if a value for that argument is not
specified in the call. Here, instead of specifying the
type of x
, y
, and diameter
, we provide them a default
value of 0.0
. Because we did not specify their type
but did provide a default value, the type of these arguments
is inferred to be the type of that value - in this case, it is
real
.
proc create_circle(x = 0.0, y = 0.0, diameter = 0.0)
{
var result = new Circle();
result.radius = diameter / 2;
result.center.x = x;
result.center.y = y;
return result;
}
writeln("Using named arguments");
Using named actual arguments in the call can prevent confusion.
Specifying that the first value provided should be used for the
argument diameter
allows us to define the arguments in any
order. Additionally, we can take advantage of the default value
for y
by not specifying a value to use instead.
Thus this call creates a circle at (2.0, 0.0)
with a radius
of 1.5
.
var c = create_circle(diameter=3.0,2.0);
writeln(c);
writeln();
Procedures can also have arguments of indeterminate type: these are known as generic procedures.
proc unknownArg(x)
{
writeln(x);
if x.type == int then
writeln("I see you've passed me an integer!");
else if x.type == string {
writeln("I liked that last variable so much, I'll write it again!");
writeln(x);
}
}
var intArg = 5;
var strArg = "Greetings, procedure unknownArg!";
var boolArg = false;
writeln("Using generic arguments");
unknownArg(intArg);
unknownArg(strArg);
unknownArg(boolArg);
writeln();
Argument Intents¶
Normal (default) intent means that a formal argument cannot be modified
in the body of a procedure.
To allow changing the formal (but not the actual), use the in
intent.
config param useSleep = true; // Set at compile time, used to speed up testing
use Time;
proc countDown(in n : uint = 10) : void
{
while n > 0
{
writeln(n, " ...");
if useSleep then sleep(1);
n -= 1;
}
writeln("Blastoff!");
}
writeln("Using the \"in\" intent");
var s = 5 : uint;
countDown(s);
writeln("s is still ", s); // 5
writeln();
The inout
intent will write back the final value of a formal parameter
when the procedure exits.
proc countDownToZero(inout n : uint = 10) : void
{
while n > 0
{
writeln(n, " ...");
if useSleep then sleep(1);
n -= 1;
}
writeln("Boink?");
}
writeln("Using the \"inout\" intent");
var t = 3 : uint;
countDownToZero(t);
writeln("t is now ", t); // 0
writeln();
Similar to the inout
intent, the ref
intent causes the value of
the actual to change depending on the function. However, while the
inout
copies the argument in upon entering the function and copies
the new value out upon exiting, using a ref
intent causes any updates
to the formal to immediately affect the call site.
proc countDownToZeroToo(ref n : uint = 10) : void
{
while n > 0
{
writeln(n, " ...");
if useSleep then sleep(1);
n -= 1;
}
writeln("Flippity boop");
}
writeln("Using the \"ref\" intent");
var bip = 3 : uint;
countDownToZeroToo(bip);
writeln("bip is now ", bip); // 0
writeln();
The out
intent causes the actual argument to be ignored when the
procedure starts. The actual is assigned the value of the corresponding
formal when the routine exits.
This arctan routine puts the result in the argument with out
intent
and returns the number of iterations it needed to converge.
atan x = x - x^3/3 + x^5/5 + sum_3^inf (-1)^i x^(2i+1)/(2i+1)
.
This actually converges very slowly for x close to 1 in absolute value. So we set the error limit to be 3 significant digits.
proc atan(x : real, out result : real)
{
result = 0.0;
var count = 0;
var lastresult = 0.0;
for i in 1.. by 2
{
var twoIP1 = 2 * count + 1;
var term = x ** twoIP1 / twoIP1;
result += if count % 2 == 0 then term else -term;
count += 1;
if abs(result - lastresult) < 1.0e-3 then break;
lastresult = result;
}
return count;
}
writeln("Using the \"out\" intent");
var theta : real;
var n = atan(1.0, theta);
writeln("Computed Pi as about ", 4.0 * theta, " in ", n, " iterations.");
writeln();
A procedure can take a variable number of arguments – of indeterminate type. It is expanded like a generic procedure, with the required number of arguments having types which match the actual arguments.
Note: see the Variadic Arguments primer for further information on procedures with a variable number of arguments
proc writeList(xs ...?k) {
var first = true;
for x in xs {
if first then first = false; else write(" ");
write(x);
}
writeln();
}
writeln("Using variable argument lists.");
writeList(1, "red", 8.72, 1..4);
writeln();