Functions are defined with the fn
keyword. The return type of the function must be declared if a function returns any value.
fn <name>( <type> arg1, <type> arg2, ... ) -> <return type> {
// Arguments are immutable
// Any variables declared inside are scoped to the function
}
Example function:
fn hello(int a, int b) -> str {
a = 2 // Error
mut int b = b // Creates new scoped variable `b`, let's call it `bscope`.
b = 2 // This will edit `bscope`, not the argument value `b`
return "Hello, world!"
}
// `bscope` is not available here
str hello = hello(a, b) // pass in `int a` and `int b` from above
print(hello)
fn add(int a, int b) -> int {
return a + b
}
fn addA = add(1) // Partially applied function. `addA` now has the `int a` parameter of `add` prefilled, and only takes one parameter `int b`
int c = addA(2) // 3
This can be useful to create functions from a base “template” function. For example, if you were creating an HTML library, you might use a function el(str tag, str content)
as a base, and build all of your elements off of that:
fn el(str tag, str content) -> str {
return "<{tag}>{content}</{tag}>"
}
fn html = el("html") // fn html(str content) -> str
fn body = el("body") // fn body(str content) -> str
// ...
body("Hello World") // <body>Hello World</body>
Functions can be overloaded with different implementations of the same function. This is usually helpful when you want to have multiple parameter types, for example. However, because the language is strictly typed, each overload must have its own implementation.
fn sqrt(int n) -> decimal {
// implementation for int
}
fn sqrt(decimal n) -> decimal {
// implementation for decimal
}
You can even have functions with the same input parameters, since the return type can determine which function implementation was used.
fn sqrt(int n) -> int {
// returns int
}
fn sqrt(int n) -> decimal {
// returns decimal
}
You can pass a function as an argument to other functions, or assign them as values to variables.
// Don't need the full function signature in the type as this is a function definition
fn add = fn(int a, int b) -> int {
return a + b
}
// This function takes a function `operation` as its third parameter
// `operation` should be a function that takes in 2 int values and returns an int value
// Any function that doesn't match this signature will cause an error
fn calculator(int a, int b, (fn(int, int) -> int) operation) -> int {
return operation(a, b)
}
int answer = calculator(3, 5, add)
answer == 8
Whenever a function signature is used as a type, it must be surrounded with parentheses (fn(<type> arg1, <type> arg2, ...) -> <return type>)
. For example:
struct Calculator {
(fn(int, int) -> int) add,
(fn(int, int) -> int) substract,
(fn(int, int) -> int) multiply,
(fn(int, int) -> int) divide,
}
If a function throws an error, it must be declared in the function definition with an exclamation mark on the return type:
fn addThrows(int a, int b) -> int! {
if a < b {
panic("first argument cannot be less than second argument")
}
return a + b
}
Throwing functions must be called with try
:
int c = try addThrows(3, 5) // throws
int d = try addThrows(5, 2)
d == 7
Any function that calls another function marked with try
must itself be declared as a throwing function.
fn calc(int a, int b, str op) -> int! { // marked as throwing
match op {
"+" { return try addThrows(a, b) }
}
}
If a function throws but does not return anything, it can be marked as throwing with -> !
:
fn isValid(str source, str check) -> ! { // marked as throwing without return value
if source != check {
panic("Not valid")
}
}
Functions that throw will immediately exit out of the program if they error. If you want to handle errors differently, you must return them as values from the function, usually in a tuple:
fn addError(int a, int b) -> (int?, error?) {
if a < b {
return (undefined, error("first argument cannot be less than second argument"))
}
return (a + b, undefined)
}
int? c, error? err = addError(3, 5)
if err != undefined || c == undefined {
print(err)
}
// `c` is of type `int` here
Alternatively, you can handle the error inside the function, and simply return undefined
:
fn addUndef(int a, int b) -> int? {
if a < b {
print("first argument cannot be less than second argument")
return undefined
}
return a + b
}
int? c = addUndef(3, 5)
c == undefined
int? d = addUndef(5, 2)
d == 7
Handling the error inside the function and returning undefined
is more correct if you’re building an application and you don’t want an error to crash the application. If you want the application to crash on error, throwing is the correct option. Finally, if you’re building a library to be used by other people, you likely want to return errors as values so that the user of your library can handle the error how they wish.
However, the language does not enforce this on you, and you are free to handle errors however you wish.
Function calls can have named arguments, though this is optional and only for clarity, or in case you’d like to pass in arguments in a different order.
fn add(int a, int b) -> int {
return a + b
}
add(1, 2) // a = 1, b = 2
add(a: 1, b: 2) // Same as above
add(b: 1, a: 2) // Can declare them in any order now since they're named
Parameters can be defined as optionals, but these cannot be excluded from the function call if they are undefined. You must pass in undefined
as an argument if you are using an optional argument.
fn optAddNum(int a, int? b) -> int {
if (b == undefined) {
return a
}
return a + b
}
int c = optAddNum(5, undefined)
Not passing the second value will create a curried function instead:
fn optAddTo5 = optAddNum(5)
int c = optAddTo5(undefined)