First-class Functions in Chapel

Note

First-class functions are under active development and are provided in their prototype form as a preview and to gather user feedback. The features described in the first part of this document are not yet stable enough to consider for use in user applications, outside of experimentation.

The first part of this document describes new prototype features which we are in the process of adding and stabilizing. The remaining sections describe legacy support for first-class functions which have been included with Chapel for many releases.

While these legacy features may be considered stable for the time being, we do not intend to advance them further, and will likely deprecate them after new prototype features become stable. These sections have had their headers prefixed with ‘Legacy’ to indicate that they describe legacy syntax or semantics.

New Syntax for Constructing Function Types

Warning

New features added to support first-class functions may not be supported by default. In order to use them, it may be necessary to set the config param fcfsUseLegacyBehavior to false.

Function types may now be constructed using new syntax which mirrors the syntax for function definition.

To construct the type of a function which takes two integers and returns an integer, users may write the following:

type T = proc(x: int, y: int): int;
writeln(T:string); // 'proc(x: int, y: int): int'

Notice that the formals of the function type have specified names, x and y. Currently, formal names participate in typing such that a function defined with formals named x and y cannot be assigned to a function defined with different formal names. In the below example, assignment of two function values will fail because the formal names are different:

proc foo(x: int, y: int): int { return x + y; }
proc bar(a: int, b: int): int { return a + b; }
writeln(foo.type:string);   // 'proc(x: int, y: int): int'
var x = foo;                // ^ (same type) ^
writeln(bar.type:string);   // 'proc(a: int, b: int): int'
x = bar;                    // Error!

There are scenarios where a user may want to pass around functions without regard for their formal names. In such cases, anonymous formals may be used instead.

Formals in Function Types May Be Anonymous

A formal in a function type may be anonymous. This may be done by naming the formal _, similar to what is written to discard tuple elements when de-tupling.

An anonymous formal in a function type indicates that values of this type may have formals with any name. The formal name no longer plays a significant role in typing.

proc foo(x: int, y: int): int { return x + y; }
proc bar(a: int, b: int): int { return a + b; }

// Here the formals of 'T' are anonymous.
type T = proc(_: int, _: int): int;

var x: T = foo;             // OK, T's formals are anonymous '_'.
writeln(foo.type:string);   // 'proc(x: int, y: int): int'
writeln(x.type:string);     // 'proc(_: int, _: int): int'
writeln(bar.type:string);   // 'proc(a: int, b: int): int'
x = bar;                    // OK!

In the above example, two functions are declared with different formal names, but otherwise identical types. A local variable expresses a function type with anonymous formals, which enables the two functions to be assigned freely to the variable.

Note

Currently, it is not possible for function definitions to declare anonymous formals. It has been indicated that such a feature might be useful, so this may change in the future.

New Syntax for Constructing Anonymous Functions

A new syntax for constructing anonymous functions has been introduced which more closely mirrors traditional function definition.

// Define a function named 'foo'.
proc foo(x: int, y: int): int { return x + y; }

// Define an anonymous function bound to the constant variable 'bar'.
const bar = proc(a: int, b: int): int { return a + b; };

Legacy: 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

  • A function referring to outer variable, other than globals

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. Functions referring to outer non-global variables are not currently supported in the implementation.

Legacy: 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

Legacy: 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);

Legacy: 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);

generates the output:

55

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

Additionally, first-class functions can be cast to a string to get the function name or printed to output the function name. For example:

proc myFunc(x:int) { return x + 1; }

var F = myFunc;
var Fname = F:string;

writeln(Fname);
writeln(F);

generates the output:

myFunc()
myFunc()

Future Directions

New features for first-class functions are under active development as of release 1.29. If you have specific feature requests or suggestions, please let us know on the Chapel GitHub issues page or community forums.