This chapter explains how to declare and implement functions which transform provided parameters into returned value(s). Here's a simple example of function definition and 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)
This example shows the key parts of an function definition:
- The signature which declares its:
- name (square in this example).
- parameters in parentheses (a in this example).
- return type which follows the parentheses (f32 in this example).
- The statement block follows the signature. It contains indented statement(s) that representing the logic to be performed by the function.
The declaration of a functions's parameters is a comma-separated list of variable names, enclosed in parentheses. Each parameter variable corresponds on a one-to-one basis to the values passed to the function when it is called. They must match in both number and type:
// Define a method 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
For those wondering, this number and type restriction means Cone does not support:
- Variadic parameters, where a varying number of parameters can be passed to a function. To achieve a similar result, make use of collections. In particular, the << operator provides a more flexible way of building a string value than the traditional "printf" function.
- Ad hoc polymorphism, where multiple functions can be defined with the same name, each with a different type signature. To some degree, using same-named methods over differently typed objects offers a similar capability.
Parameter variables are local variables
Within the statement block, a parameter variable is treated just like a local variable. Indeed, the declaration for each parameter looks very much like a declaration for a local variable, with the value type being specified after the variable's name. In the common case where the permission is omitted, the imm permission is assumed. That restricts the function's logic from changing the value of a passed parameter.
To allow a parameter's value to be changed, precede the name with mut. Unless references (pointers) are involved, mutation of the parameter's value will have no effect on the caller since the parameter's value is a copy of the caller's value. Changes to one value do not affect the other.
fn weird(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 weird(5, number) // number is still 3, despite weird() changing b to 4
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 literal value.
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
The indented statements that follow the function's signature are its statement block. The block consists of a sequence of statements that represents the function's logic. Generally, each statement is performed in order.
Many statements are expressions (including assignment expressions). However, there are also several special-use statements that begin with a reserved keyword (e.g., return). These special-use statements will be introduced in later sections.
In addition to function blocks, several other kinds of statement blocks can be incorporated in a function's statement block. These too typically begin with a reserved keyword. For example, the if and while control blocks are used to control the flow of execution based on calculated conditions. Use of such blocks further indents code statements. Such statement blocks are considered part of the logic of the function they appear within.
Local variable declaration
As described earlier, declarations for local variables are often found within a function's statement block. They are distinguished from other statements by starting off with a permission, typically imm or mut. The usable lifetime (scope) for these variables is the block they are declared within. Space for them is made available when the block is entered and disappears when the block is left.
fn summult(a f32, b f32) f32 imm sum, mult = a + b, a * b // local variable declaration return sum / mult
A return statement may be placed anywhere within a method (even within a control block). When encountered, execution of the method ceases and all comma-separated values specified after return are returned to the caller. 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 mut a,b = ceil(8) // returns 8,6 a,b = ceil(3) // returns 3,3
A function does not have to end with a return statement. If the function signature does not declare a return value, the function automatically returns after the last statement is performed.
If the function signature declares that values must be returned, an attempt is made to find matching 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
fn newthing() Thing +Thing // implicitly returns this newly created 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.
A function may call itself recursively:
fn factorial(x i32, prod i32 = 1) return prod if x<=1 factorial(x-1, prod*x)
If the function returns a single value calculated by any function, this will be "tail-call" optimized. This optimization improves performance and reduces the risk of execution stack overrun with recursive calls.
Several other language features, described elsewhere, look and act like functions:
- methods, which operate in the context of an object
- anonymous (first-class) functions
- closures, which allow a function to preserves its state
- co-routines, which preserve a function's data and execution state
- behaviors, like methods for threads/actors