Cone Language Reference

Functions

On this page

Note: All of this capability is implemented except throws and pure functions.

All processing logic (including expressions and blocks) for programs are specified inside functions. We have already shown how to call functions as part of an expression. Now, let's show how to declare and implement functions?

Let's start with a simple example of a function definition and its use:

// Define the 'square' function
fn square(a f32) f32 {
	return a*a;

// Use square to calculate the area of a circle	
}
area = Float.pi * square(r);

The function definition for 'square' specifies its:

  • Name ('square').
  • Signature which declares its:
    • typed parameters in parentheses (a in this example). These are the named values sent to the function when called.
    • return type(s) which follows the parentheses (f32 in this example). These are the types of any values calculated by the function and sent back to the caller.
  • Implementation block following the block marker (':'). The block contains statement(s) that represent the logic to be performed by the function.

Notice how the function call passes the value 'r' to the 'square' function. This value is caught by the function as the floating-point parameter 'a'. The function body multiplies 'a' to itself, and then returns the calculated floating-point value to the caller.

Function name#

The function name is typically simple: the unique name for the function, used when calling the function. If a function name requires non-alphanumeric characters, enclose the name in back ticks (for example, `+` is a valid name for an operator function which adds values).

Overloaded functions and their names#

Every function has its own name, unique within its namespace. A function may additionally declare that it overloads a shared name, by placing the overload keyword and that shared name between its own name and its signature. Overloading is useful when several functions perform essentially the same semantic operation, but do so using a different number or different types of parameter values.

// 'addFloat' is this function's own name. It also overloads the shared name 'add'.
fn addFloat overload add(a Comp, b f32) Comp {
  ...

// 'addDyn' is this function's own name. It overloads the same shared name.
}
fn addDyn overload add(a Comp, b dyn) Comp {
   ...
}

A shared name may only be used as the name of a function being called. A function's own name has no such restriction: it is the name to use when obtaining a reference to the function (e.g., by '&'), and it is the basis for the function's generated linker name. A shared name is as visible as the functions that overload it, so they must agree: the first declared decides whether the shared name is pub, and a later function that disagrees is a compile error, because callers of a public name would otherwise reach code its own declaration keeps private, or a public function would be hidden behind a private name.

When a shared name is called, the compiler tries every function that overloads it and requires exactly one of them to accept the call's arguments, allowing for default parameter values and permitted implicit coercions. No matching function is an error, and more than one matching function is an ambiguity error. An exact match is not preferred over a match requiring coercion, needing fewer coercions does not win, declaration order never breaks a tie, and return types take no part in the choice. A caller resolves an ambiguity by calling a function's own name or by converting arguments so that only one function accepts them.

Two functions may not declare the same own name, a shared name may not also be some other declaration's name, and two functions that overload one shared name may not accept exactly the same arguments.

A generic function may declare a shared name only when every one of its type parameters is bound to a function signature, as in fn taperBy[F fn(u f32) f32] overload taper(m &mut Mesh, f &F). Its &F (or &mut F, or F) parameter then counts as a callable: a call accepts the generic when the argument there is a closure, a function reference or a struct with a () that meets the signature, and the type parameter is inferred from it as it is for a call by the generic's own name. A closure literal is matched, as it is against any overload, by the parameters that take a callable and then by its number of parameters, so taper(m, 0.5) reaches an overload taking a number and taper(m, t => t * k), a closure that borrows k, reaches the generic. Two generics bound by one signature, or by signatures a literal does not tell apart, are an ambiguity error that names both. Any other generic, such as [T] or [T Comparable], may not declare a shared name: nothing says how to rank it against the rest.

Parameter Variables#

The declaration of a functions's parameters is a comma-separated list of typed variables (name and type), enclosed in parentheses. Each parameter variable corresponds on a one-to-one basis to the value arguments passed to the function when it is called. They must match in both number and type:

// Define a function that returns the addition of its two parameters
fn add(a i32, b i32) i32 {
	return a+b;
}

add(2,3);  // returns 5, since a is set to 2 and b to 3
add(2);    // ERROR! add() needs exactly two values, not one or three
add(2.0, 4.0);  // ERROR! add() needs integers, not floating point numbers

Parameters are local variables, usable within the function. Each parameter declaration in the function's signature mimics a local variable declaration, even allowing specification of mutability and a default value.

Parameter Mutability#

When no permission is specified before the parameter name (as in the example above), imm is assumed. This forbids the function's logic from changing the value of the passed parameter.

To allow a parameter's value to be changed within the function's logic, precede the name with mut. Any change to the parameter's value in the function logic will have no effect on the caller, since the parameter's value is a copy of the caller's passed value.

fn mutator(a i32, mut b i32) {
	a = 34;    // ERROR! a is immutable and may not be changed
	b = 4;     // b may be changed since it is declared 'mut'
}

number = 3;
mutator(5, number);
// number is still 3, despite mutator() changing b to 4

The compiler may pass a large value without copying it when nothing can change it during the call. The function still sees exactly the value its caller passed.

Parameter Defaults#

If desired, default values may be specified for any parameter value using the assignment operator '='. The default value is what we want the parameter to have if the caller provides no value for it. The default value can only be a constant value: a literal, a named constant, or a constant expression computed from them. Once a parameter has a default, every parameter after it must have one too, and a call may leave off any trailing run of the arguments that have defaults.

fn next(nbr i32, incr i32 = 1) i32 {
	return nbr + incr;
}

next(5,2); // returns 7
next(4);   // returns 5 (using incr's default value of 1)
next();    // ERROR! no default value for nbr

fn span(start i32 = 0, len i32 = 10) i32 {
	return start + len;
}

span();      // returns 10
span(5);     // returns 15
span(5, 2);  // returns 7

A default may be a named constant or an expression of constants, and a string literal is a constant like any other:

const step = 3;

fn bump(n i32, amt i32 = step * 2) i32 {
	return n + amt;
}

fn greet(name &str = "world") {
	print <- "hello, ", name, "\n";
}

bump(1);         // returns 7
greet();         // prints: hello, world
greet("there");  // prints: hello, there

Return types#

This comma-separated list specifies the type(s) of all values returned by the function:

// A function that returns a single integer value
fn getint() i32 {

// A function that returns both an integer and float tuple value 
}
fn gettuple() i32, f32 {

// A function that returns no values (equivalent to returning 'void'):
}
fn get() {
}

The commas separate return types only where the signature stands on its own: a function's own signature, or a function type given as a field's or a variable's type. Inside a list — a parameter list, a function type's own parameter list, or any other list in parentheses or brackets — a comma after a function type's return type continues the list instead. A function type there that returns several values puts them in parentheses, as a tuple:

// f takes a u32 and returns one u32; x is a second parameter
fn apply(f &fn(u32) u32, x u32) u32 {

// g returns two values, so they are written in parentheses
}
fn applyBoth(g &fn(u32) (u32, u32), x u32) u32 {
}

throws may be specified after the return types to show that a function may throw an error value (using throw instead of return) if something goes wrong. See Result for more information.

// returns an integer on success, or else throws ErrorCond value for an exception
fn dosomething() i32 throws ErrorCond;

Implementation Block#

The function's implementation block is like any regular block, consisting of a ordered sequence of statements that perform the function's logic.

Local variables#

All local variables declared within the function's block (including the function's parameters) provide a temporary working state for the function. Every time a function is called, space is allocated on the execution stack for all its declared local variables. When the function is finished and returns to its caller, its local variable space is automatically freed from the stack.

fn summult(a f32, b f32) f32 {
	imm sum = a + b;    // local variable declaration
	imm mult = a * b;   // local variable declaration
	return sum / mult;
}

This means that local variables are exclusive to that call. Two identically-named local variables in different functions, or even two calls to the same function, will not collide with each other.

Return Statement#

A return statement may be placed at the end of any block. When encountered, execution of the function ceases and the comma-separated values specified after return are returned to the caller. The number and types of all return values must match the return types declared on the function's signature.

fn ceil(x i32) i32, i32 {
	if x > 6 {
		return x, 6;
	}
	return x, x;
}

a,b = ceil(8);       // returns 8,6
a,b = ceil(3);       // returns 3,3

Implicit returns#

A function does not have to specify a return statement at the end of its main block. If the function signature does not declare a return value, the function just returns after the last statement is performed.

If the function signature declares that values must be returned, an attempt is made to matching the values on the last statement or block of the function:

  • if it is an expression whose types match, it is treated as if return were specified before the expression:
    fn ceil(x i32) i32, i32 {
    	if x > 6 {
    		return x, 6;
    	}
    	x, x;   // implicit return
    }
    
  • An if or match returns the last statement's expression in each of its distinct blocks (this is handled recursively). All paths (including a required else block) must return valid values.
  • fn ceil(x i32) i32, i32 {
    	if x > 6 {
    		x, 6;   // implicit return
    	} else {
    		x, x;   // implicit return
    	}
    }
    
  • A final with block returns the value of this.
  • fn newthing() Thing {
    	with thing {    // implicitly returns thing
    		.name = "Doofus";
    		.color = "brown";
    	}
    }
    

Should the correct number of type-matching return values not be found as described above, a compile error will result.

Recursive Functions#

A function may call itself recursively:

fn factorial(x i32, prod i32 = 1) {
  if x<=1 {
    return prod; 
  }
  factorial(x-1, prod*x);
}

If the function returns a single value calculated by any function, this may be "tail-call" optimized. This optimization improves performance and reduces the risk of execution stack overrun with recursive calls.

Pure functions#

Sometimes it is valuable when a function guarantees that its only work involves calculating return value(s) derived wholly from the information passed as parameters. Such functions are called pure. Pure functions may not:

  • Access global, mutable variables, except to use regions to allocate memory.
  • Call functions or methods that are not pure. This effectively prevents the function from doing any I/O, since that requires use of functions that won't be marked as pure.

Functions complying with these constraints should be marked as pure.

pure fn factorial(mut n i32) i32 {
  if n<=1 {
    return 1; 
  }
  mut result = 1;
  while n-- > 1 {
    result *= n;
  }
  result;
}

A key benefit for marking functions pure: it makes code easier to understand, debug and test. When examining logic that uses a pure function, we can be confident that:

  • Passing it the same data will always return the same results.
  • Using it will not trigger some unexpected state-changing behavior (side-effect), such as performing any sort of i/o.
  • It will not mutate any state other than objects it has explicitly been given mutable references to.

Pure functions are also beneficial due to the safety guarantees they offer when:

Strong purity#

The compiler only enforces the purity constraints listed above, resulting in what some may consider a weak form of purity. A stronger form of purity (referential transparency) may be obtained by ensuring:

  • none of the parameters are mutable references or pointers.
  • the logic never uses potentially non-deterministic operations, such as comparing pointers or re-casting machine-specific value encodings to other types.

Only a strongly pure functions can be safely memo-ized.

Inline functions#

A function can be marked as "inline". For example:

fn incr(x i32) inline {
  x+1;
}

Any time another piece of code calls an inlined function or method, the called function's logic is actually generated within the caller's logic. For small inlined functions, this can offer a significant performance advantage, not only because it eliminates the overhead of call/return, but more importantly because it is very instruction-cache friendly.

Note: It is impossible to borrow a reference to an inlined function.