First-class Functions in Chapel

This README describes some initial support that we have added to the compiler for first-class functions in Chapel. This mechanism should be considered a stopgap technology until we have developed and implemented a more robust story, which is why it's being described in this README rather than the language specification.

Manipulating first-class functions

Functions defined with parentheses may be captured as values by referring to them by name without parentheses. Once captured, these values may be passed around as other value types.

For example:

proc myfunc(x:int) { return x + 1; }
var f = myfunc;
writeln(f(3));  // outputs: 4

To be captured, a function must not be any of the following:

  • A generic function (all captured functions must be fully-qualified with no generic arguments)
  • A function with special return types (type, param)
  • An iterator
  • The method of an object
  • An operator
  • An overloaded function

Rationale. Generic functions would require manipulating generic, uninstantiated types, which is currently not available in Chapel. Functions with compile-time return types like type and param would require the ability to have param classes, to fit with the current implementation. Param classes are not currently part of Chapel. Iterators would require a new type of capture, one that works similarly to the current implementation but respects the yielding that occurs inside an iterator. Method capture requires the currying of the object as the first argument to the first-class function. Operators and overloaded functions require a type-based multiple dispatch mechanism.

Lambda functions

Lambda functions are anonymous first-class function objects. In other words, they are expressions rather than formally-defined named functions. They are available with the following syntax:

lambda-declaration-expression:
  lambda argument-list return-type_opt function-body

where lambda is a Chapel keyword and return-type_opt is an optional return-type.

For example:

var f = lambda(x:int, y:int) { return x + y; };
writeln(f(1,2));  // outputs: 3

Lambdas can also "capture" variables that are defined outside of the lambda by referring to them in the body of the lambda. These form a "closure", a combination of a function and an associated execution environment. This closure captures the variables in such a way that modifying them modifies the original variables (this is sometimes called "capturing the variables by reference"). Currently a lambda that captures variables can be used only while the activation records of the functions that define these variables, if any, still exist on the stack, i.e. while these functions are still executing.

Example. For example the following is acceptable:

proc myfunc() {
  var x = 3;
  var f = lambda() { x = 4; };

  f();

  return x;
}

writeln(myfunc());  // outputs: 4

whereas having myfunc() return its f variable is not supported.

Specifying the type of a first-class function

The previous examples rely on type inference to determine the type for those variables that can be assigned to a first-class function. Chapel provides three type functions, all named func, that return the type that corresponds to a function signature as follows:

// Returns the type for a function of no arguments and void return type (returns no value)
proc func() type

// Returns the type for a function of no arguments and return type 'retType'
proc func(type retType) type

// Returns the type for a function with arguments argTypes and return type 'retType'.
proc func(type argTypes...?n, type retType) type

These can be used to declare the type of a variable that can be assigned to values of a function type. For example:

 // Two ways to define a function with no arguments and no return value
 var f1: func();
 var f2: func(void);

 // A function with no arguments, returning int
 var g : func(int);

// A function with two bool arguments, returning int
 var h : func(bool, bool, int);

Reflection

First-class functions define a type method retType that returns the type of the value that would be returned if the function were to be invoked, and a type method argTypes that returns a tuple of the types of each formal.

For example:

var F = lambda (x: int) { return x + 42; };

writeln(F(13));
writeln();

writeln("retType  = ", F.retType  : string);
writeln("argTypes = ", F.argTypes : string);
writeln();

generates the output:

55

retType  = int(64)
argTypes = 1*int(64)

Future Directions

Over time, we will be improving the support for first-class functions and their syntax. If you have specific feature requests or suggestions, please let us know at: chapel_info@cray.com.