Function
Functions are self-contained chunks of code that perform a specific task. You give a function a name that identifies what it does, and this name is used to call the function to perform its task when needed.
Like many other functional programming languages, functions are first-class citizens of Atem. You can assign them to the variables, pass them as parameters, return them from functions, or declare nested functions. The unified syntax of declaring variables and functions also implies the similarity between them:
//a variable
foo: Int8 = 3;
//a function
bar: func = {};
Every functions in Atem has its own types, consisting of the function's parameter types, return type, annotations, and contracts. You can use these types like any other types in Atem.
Defining and Calling Functions
Basically, a function definition contains the following parts (The list is ordered):
- Function Name, the function names (usually) describes what functions do.
- Column
:
, indicating you are defining some object. - (Optional) Contracts, providing function's requirements of arguments.
- (Optional) Annotations, transforming the function behavior.
- (Optional) Parameter List, describing what argument the function would take.
- (Optional) Function Literal "Arrow"
->
, indicating that we are defining some "mapping" between function parameters and function return values. - (Optional) Return Type, describing what function will return.
- Function Declarator
func
, indicating that we are defining a function. - Assignment
=
, indicating that you are "assign" a function body to a "variable". - Function Body, containing the function's code.
- Semicolon
;
, indicating we are done with current statement.
Despite being a long list, many items of the list is optional and can be omitted if you don't need them. A simplest function definition just need to contains 1. 2. 8. 9. 10. 11. Consider the following example:
//The simplest function definition you could write in Atem
simplest: func = {
println("Hello World!");
};
In the example above, the defined a function named "simplest". The function simplest
takes no parameters and return no values. When called, the function will output "Hello World!"
in the console.
To call our function, we need to use the function name and an argument list:
simplest();
The line above will call our simplest
function.
Function Parameters
Real-world functions usually takes lots of parameters to work. Luckily, Atem provides a well-defined syntaxes and mechanics to handle the most use cases well.
Typically, a function parameter is consisted of (The list is ordered):
- (Optional) Parameter Label, the name for parameter in the function calling.
- Parameter Name, the name for parameter in the function body.
- Column
:
, indicating we are doing some declarations. - (Optional) Parameter Directives, specifying what way will the parameter pass.
- Parameter Type, describing what type of values that the parameter will accept.
To add parameters to a function, you will need to write a function type literal () -> ()
before the function declarator:
simple_print: (msg: String) -> () func = {
println(msg);
};
You can add multiple parameters to the function by separating parameters with comma ,
in the parameter list:
complex_print: (msg: String, terminator: String) -> () func = {
print("msg$terminator$");
};
Named Function Parameters
For now, the only way to recognize the usage of each argument when calling a function is just by the order of the argument. Consider the power
example:
power: (base: Float64, exp: Int32) -> Float64 func = {
return base**exp;
};
Because there is no obvious way to distinguish the two arguments, we could get surprising results when we accidently pass the arguments in wrong order:
eight := power(3, 2); //Oops, means 2**3, but accidently get 3**2!
We can avoid the problem by specifying argument labels explicitly:
eight := power(exp = 3, base = 2); //Good, the intent is much more clear
You may notice for now the argument labels are still optional, which also may lead to confusion and errors. To make the argument labels mandatory, you need to add parameter labels to the parameters:
betterPower: (powerBase base: Float64, powerExp exp: Int32) -> Float64 func = {
return base**exp;
};
Now the parameter names are consisted of two parts:
- Parameter Label (
powerBase
andpowerExp
in the example) is the name which should be explicit specified when being called. - Parameter Name (
base
andexp
in the example) is the name which only could be used in the function body.
The parameters which have labels, or labelled parameters, must be specified by argument labels when calling:
betterPower(2, 3); //Error
betterPower(powerBase = 2, powerExp = 3); //Good
Unlike unlabelled parameters, the order of labelled arguments can be arbitrary, so this is a valid function call too:
betterPower(powerExp = 3, powerBase = 3); //Good
Default Parameter Values
You can define default values for function parameters by following a assignment =
after the parameter type:
evenBetterPower: (powerBase base: Float64, powerExp exp: Int32 = 2) -> Float64 func = {
return base**exp;
};
You can omit arguments when default values is specified:
assert(evenBetterPower(powerBase = 5, powerExp = 2) == 25);
assert(evenBetterPower(powerBase = 5) == 25);
The unlabelled default parameters must be placed after all unlabelled parameters in the parameter list. If the parameters are labelled, the position of parameters will be unrestricted.
Function Return Values
Functions aren't required to have a return type. But if your function has parameter list, you still need to explicitly write the function return type (which will be void
or ()
in this case).
To return some value from function, you need to specify a type to describe what type of value the function will return. Consider the previous power
example:
power: (base: Float64, exp: Int32) -> Float64 func = {
return base**exp;
};
When a function has a return type, every possible return value should be the same type or at least implicit convertible to the return type, otherwise a compile error will occur:
error_return: (typename: String) -> Int32 func = {
typename match {
"Int8" = {return 3::Int8;} //Good, the returned value is implicitly convertible to the return type
"Int32" = {return 3::Int32} //Good, the returned value is the same type of return type
"Bool" = {return true;} //ERROR, Type "bool" is not implicitly convertible to the return type "Int32"
};
};
If a function is called with return values be ignored, a error will occur:
sin(1); //ERROR, Ignoring return values from function call "sin(1)"
To avoid this, you can assign the return value to the placeholder _
:
_ = sin(1);
Or add a @noReturnCheck
annotation to the function:
sin: @noReturnCheck (value: Float64) -> Float64 = {...};
sin(1);
Or just turn the safety profile off:
@compileTimeSafetyProfile(safety.compiletime.ruleset.no_return_value_ignorance, false);
sin(1);
You can use a tuple type as the return type to enable returning multiple values.
The pairFinder
example defines a function that will find a matching key pair in the given map and return the key and value in the tuple form:
pairFinder: (key: String, map: [string]Int32) -> (key: String, value: Int32) func = {
for([k, v] in map) {
if(k == key) {
return .{key = k, value = v};
}
}
};
You can also express null
state of return value by using optionals, consider a enhanced version of pairFinder
:
betterPairFinder: (key: String, map: [string]Int32) -> ?(key: String, value: Int32) func = {
for([k, v] in map) {
if(k == key) {
return .{key = k, value = v};
}
}
return null;
};
Expression Function Body
If the function body is a single expression, then the function will return the expression's value, no explicit return
is needed. This makes some simple arithmetic functions more convenient to write, like:
plus: (a: Int8, b:Int8) -> Int8 func = a + b;
To keep syntax consistent, you can also use a block to contain the expression:
plus: (a: Int8, b:Int8) -> Int8 func = {a + b};
Function Types
Every function has its specific function type. A function type is consisted of:
- Parameter Types
- Return Type
- Annotations
- Contracts
The parameter types includes:
- Types and order of unlabelled parameters
- Labels and types of labelled parameters
A function type literal is represented in the syntax Annotation_opt Contract_opt (ParameterList) -> ReturnType
.
Consider the following example:
comptime assert((i8, bool) -> () != (bool, i8) -> ());
comptime assert((a: i8, b: bool) -> () == (b: bool, a: i8) -> ());
comptime assert((a: i8, b: bool) -> () != (c: i8, d: bool) -> ());
comptime assert((a: i8, bool, string, b: char) -> () == (bool, b: char, string, a: i8));
Function Types as Parameter Types
You can use a function type as a parameter type for another function. This enables you to leave some aspects of a function's implementation for the function's caller to provide when the function is called, or higher-order functions. Being able to passing functions as parameter enables us to much more flexible algorithm, consider the maximum
example:
maximum: (ints: ... Int8, comparator: (Int8, Int8) -> bool) function = {
ans mutable := 0::Int8;
comptime for(elem in ints) {
if(comparator(elem, ans)) {
ans = elem;
}
}
return ans;
};
Then we can call the function with customized comparator:
assert(maximum(ints = 5, 3, 6, 1, 2, 4, comparator = {%0 > %1}) == 6);
Function Types as Return Types
You can return functions from function by making a function type being the return type. The returned function type is presented in the form of function literals:
simpleAllocator: (bytes: Int8) -> (ptr: byte.&) -> () func = {
buffer: byte.& = new byte[bytes];
cleanAndFree: (ptr: byte.&) -> () func = {
delete ptr;
};
return clean_and_free;
};
Now we can receive a function from the simpleAllocator
:
cleaner := simpleAllocator(8);
cleaner(); //call the returned function
Nested Functions
All of the functions you have encountered so far in this chapter have been examples of global functions, which are defined at a global scope. You can also define functions inside the bodies of other functions, known as nested functions.
Nested functions are hidden from the outside world by default, but can still be called and used by their enclosing function. An enclosing function can also return one of its nested functions to allow the nested function to be used in another scope. The previous simpleAllocator
has already illustrated the usage of the feature:
simpleAllocator: (bytes: Int8) -> (ptr: byte.&) -> () func = {
buffer: byte.& = new byte[bytes];
cleanAndFree: (ptr: byte.&) -> () func = {
delete ptr;
};
return clean_and_free;
};
cleaner := simpleAllocator(8);
cleaner(); //call the returned function
Unified Function Call Syntax
The Unified Function Call (UFC) is a mechanism that allowed a free function to be called like a member function. UFC would apply to any function call obj.func()
which satisfies the following requirements:
obj
hasn't any appropriate member function to be called.- A free function's first parameter type matches the type of
obj
That mean the following code is valid in Atem:
add: (a: Int32, b: Int32) -> Int32 func = a + b;
main: func = {
i := 14::Int32;
//the member function call will be interpreted as "add(i, 100)"
println("{}", i.add(100)); //prints 114
};
Functional Programming Support
Pure Functions
Partial Application
Atem support partial application for functions, which means when you provide a function with some of it parameters, a new function which receiving unprovided parameters is created. Consider the example:
average: (a: Int32, b: Int32) -> Int32 func = (a + b) / 2;
main: func = {
partial_application := average(3);
println("{}", partial_application(5)); //prints 4
};
Pipeline Operator
Pipeline operators enable us to make function calls much more flexible by "piping" values into function call expressions. For example, the ordinary function call func(v)
can be rewritten to v /> func()
or v \> func(%)
. We call the former left-threading pipeline operator, the latter placeholder pipeline operator:
Code | Evaluation | Code | Evaluation |
---|---|---|---|
x /> f() | f(x) | x \> f(%) | f(x) |
x /> f(y) | f(x, y) | x \> f(y, %) | f(y, x) |
x /> f | Error, f is not a call expression | x \> f(%, y) | f(x, y) |
x \> f(%, %) | f(x, x) | ||
x \> % + y | x + y | ||
x \> f(y) + g(%) | f(y) + g(x) | ||
x \> return %; | return x; |
The pipeline operator also provide us another way for function chaining like UFC:
//Using UFC:
v :=
r.transform(f)
.filter(g)
.reverse();
//Using Pipeline:
v :=
r \> transform(%, f)
\> filter(%, g)
\> reverse(%);
//or
v :=
r /> transform(f)
/> filter(g)
/> reverse();
The three example above will all resolve to v := reverse(filter(transform(r, f), g));