Functions

How functions work in Prog.

Introduction

Functions are an important feature of Prog. A function is created by assigning a block of code to a variable, then applied with the function application operator (()). Functions can accept any number of inputs and produce any number of outputs: zero, a finite number, or an infinite number. They can be arbitrarily composed to produce new functions, and this does not preclude static typing. The compiler just takes care of it.

Table of Contents

Basic Use

A function is created by binding code to an identifier:

greet = {
    output << "Hello, world!\n";
};

The function can then be invoked using the application operator (()):

greet();

Named Parameters

If a function is assigned to a map, then the key/value information in the map can be used to supply named arguments to the function. Argument identifiers must be escaped with the escape operator (\) to prevent them from being evaluated.

greet[ \user ] = {
    output << "Hello, 'user'!\n";
};

The function is invoked, and the named parameters passed values, using ():

output << "What is your name? ";
input >> name as string;
greet(name);

If the argument identifier is typed, then the function expects a parameter of that type:

greet[ \user as string ] = {
    output << "Hello, 'user'!\n";
};

greet("Alan");    # Okay: const string to string
greet(4);         # Error: const int to string

Multiple Parameters

If the key of the map is a tuple of identifiers rather than a single identifier, the function can accept multiple parameters:

greet[ (\user as string, \days as int) ] = {
    output << "Hello, 'user'! I wish you 'days' good days!\n";
};

greet("Grace", 1024);

def

Functions that are more than trivial are more than trivial to write. To combat this and make programs more legible, Prog has the def operator, which accepts an identifier, a tuple of parameter names, and a block:

# These definitions are equivalent.

greet[ (\user as string, \days as int) ] = {};
def greet(user as string, days as int) {};

Note that it is not necessary to escape identifiers with \ when using def, nor is an equal sign (=) used. With def, unlike normal function assignment, a particular version of a function cannot be inadvertently overwritten through reassignment.

Further, while def accepts three arguments, conventionally there is no space between the function name and the parameter list. This is to be more visually consistent with the usage of the application operator.

To create a function with an empty parameter list with def, simply omit a parameter list:

def greet() {
    output << "Hello, world!\n";
};

Return Values

Often it is useful for a function to produce a result value from some input values. In Prog, a function evaluates to the last expression evaluated in the function body:

def plus_two(value as int) { value + 2 };
To exit a function prematurely and optionally return a value, use the return operator:
def plus_two(value as int) {
    return value + 2;
};

Return Types

When a function just returns a value, its return type is considered to be any, and whatever value the function returns is the value that the calling expression gets:

def gimme_a(what as string) {
    if what == "string"
        return "Here you go!";
    elsif what == "number"
        return 0xdeadbeef;
};
i as int = gimme_a("number"); # Okay
j as int = gimme_a("string"); # Error

This implicit use of any incurs dynamic typing unless the return type is not dependent on the results of any non-constant expressions (e.g., parameters). To force static typing, explicitly specify the type used when returning a value from a function by giving a pair to the def operator:

def plus_two(value as int) => int {
    return value + 2;
};

This function always returns an int, and evaluating it in any other context requires a conversion. Note that the pair operator (=>) has a higher precedence than def, which is why def func(a) => b {} is equivalent to def func((a) => b) {}.

Multiple Return Values

To return multiple values from a function, return a tuple:
def get_values() => tuple:int {
    return (3, 14, 159, 2653);
};

Named Return Values

Return values, like parameters, can be named:

# These definitions are equivalent.

def get_value() => int {
    return 3;
};

def get_value() => (result as int) {
    result = 3;
};

If one named return parameter is specified, the return type is the type of that parameter or any if unspecified. If multiple return parameters are given, the return type is a tuple of the appropriate type.

The final value of the return parameters is the value returned from the function if no other value is explicitly returned. Note that return still works if return values are named:

def get_values() => (foo as int, bar as int) {
    foo = 32;
    bar = 42;
    return (26, 39);    # Totally ignore foo and bar.
};

Variadic Functions

To create a variadic function, specify a function that accepts a tuple:

def output_spaced(arg as tuple:string) {
    output << (each arg + " ");
};

output_spaced(1, 2, 3, 4);

Any arguments given before a tuple argument are filled in from the beginning, and any following the tuple are filled in from the end:

def output_wrapped(pre as string, args as tuple:string, delim as string, post as string) {
    output < "'pre''join(delim, args)''post'\n";
};

output_wrapped("(", 1, 2, 3, ",", ")");   # Treated as variadic
output_wrapped("[", (1, 2, 3), ",", "]"); # Treated as usual

Advanced Use

Argument Overloading

Multiple definitions of a function with the same name can exist as long as they differ by the number or type of their parameters. When the function is called, the correct overload is selected according to the same criteria. The most specific overload is selected first, and an overload on any is selected last.

def spit(value as real) {
    output << "Real!\n";
};

def spit(value as int) {
    output << "Integer!\n";
};

def spit(value as any) {
    output << "Something else!\n";
};

spit(3.14);
spit(22);
spit("weird");

Variadic Argument Overloading

Multiple tuple parameters of different types can be used to make a variadic function that expects certain ranges of parameters:

def stuff(number as tuple:int, word as tuple:string) {
    output << "Numbers: " << each number <<
              "\nWords: " << each word << "\n";
};

stuff(1, 2, 3, 4, 5, "Foo", "Bar", "Baz");

Return Type Overloading

Unlike in most languages, a function can also be overloaded based on its return type, provided that context selection is unambiguous:

def get_error() => string {
    return "Error message.";
};

def get_error() => int {
    return ERROR_CODE;
};

output << "Error code 'get_error() as int': 'get_error() as string'\n";

const Overloading

Because T and const T are distinct types, functions can be overloaded based on whether a value (typically a reference) is mutable:

def next(value as int&) {
    return ++value;
};

def next(value as const int&) {
    return value + 1;
};

Composition

Two functions can be composed using standard arithmetic operators. The typical function composition rules apply, so:

def f(T => T) { ... };
def g(T => T) { ... };

(f + g)(x)   == f(x) + g(x);
(f - g)(x)   == f(x) - g(x);
f(g)(x)      == f(g(x));
g(f)(x)      == g(f(x));
(f ** n)(x)  == f(x) ** n;
f(each g)(x) == f(each g(x));

In general, functions are like iterative types: an operation on the function returns another function. The difference between a function and an iterative type is that iteration types decay implicitly through evaluation, while functions decay explicitly through invocation.