Function definition - Palamecia/mint GitHub Wiki
Functions allow you to create a reusable and parameterizable part of the script. They are introduced by the def keyword that can be followed by:
- an optional capture parameter list
- an optional call parameter list
A function always creates a block that will be executed when the function is called. When the function is called, a new context is created, and a result is always returned.
Note
A symbol name can be added before the call parameter list to automatically store the function in a global variable. This variable will also have a constant reference to avoid function redefinition but not a constant value to allow multiple signatures.
def func() {} // create the global func function
If the function definition is nested inside an other function, the symbol will be scoped to the function context.
def a() {
def b() {
return 42
}
return b()
}
a() // gives 42
b() // error: b is not defined in this scope
When a symbol name is used, a variable is created. This variable is associated with a context. When a function is called, a new context is created. This context is then empty unless a capture parameter list is used. This prevents changing the value of a variable of the caller context if a variable with the same name is used in the function.
When the function call is finished, the previous context is restored, and the script can continue as if no context change was made.
The capture parameter list allows you to store variables of the context where the function is defined in the context of the function. It starts with a [ and ends with a ] and contains a list of symbol name to capture separated by , or the ... operator to store the whole context.
a = b = c = 0
def [a, b] {} // function context includes variables a and b
def [...] {} // function context includes variables a, b and c
Variables added to the capture parameter list are shared between the definition context and the function context. Modifying a variable in the function alters the corresponding variable in the definition context.
let a = 0
def [a] { ++a } ()
a // gives 1
Important
The definition context of a function may differ from the caller context. Captured variables originate from the definition context, not the caller context.
let a = 5
let f = def {
a = 0
return def [a] { return ++a }
}
f()() // gives 1
A capture parameter can alias the result of an expression at function definition time using the = operator.
let s = 'only need this in a string'
def [c = s[9..13]] {} // function context includes variable c initialized to 'this'
Tip
The alias capture mechanism can also create global variables at the function scope.
def [g_lib = lib('my-lib')] func(a) {
return g_lib.call('func', a)
}
The call parameter list allows you to define variables that must be initialized when the function is called. It starts with a ( and ends with a ) and contains a list of symbol name to use separated by , that can end with the ... operator to allow extra parameter initialization.
def (a, b) {} // the a and b variables must be initialized at function call
def (a, b, ...) {} // the a and b variables must be initialized at function call
// and other values can be initialized too
A function can store several blocks of script while a different call parameter list is associated with it. A call parameter list is different if the number of call arguments given is different and if extra parameters are allowed or not. To add a block of script to a function, the + operator can be used on two functions.
def (a, b) {} + def (a, b, c) {} // create a function with two associated blocks
def (a) {} + def (a, ...) {} // create a function with two associated blocks
A symbol name can use an access modifier to change the attributes of the parameter variable (except for the global reference modifier). This allows preventing the copy of a constant value when passed to a constant parameter.
def (a, %b) {} // the b parameter can take a constant value without a copy
A symbol name can be followed by the = operator to add a default value to the parameter. This allows calling the function without initializing this parameter. For function call, this is like creating a new block of script where the variable is initialized inside the block.
def (a, b = 0) {} // create a function with virtually two associated blocks
Important
Only the last parameters of the list can take a default value. Once a parameter uses this mechanism, all the following parameters must use it too.
If the list of parameters ends with the ... operator, extra values can be initialized at function call. All those extra values are stored in an iterator associated with the va_args variable.
def printf(format, ...) {
print { format % va_args }
}
All functions give a result when called. By default, the none value is returned when the end of the block is reached. This result can be used by the caller.
A custom result value can be used with the return keyword followed by an expression. The result of the expression is then used as the result of the function call.
let sum = def (a, b) {
return a + b // return the sum of a and b
}
If the expression is a generator expression wrapped in parentheses, the generated iterator itself becomes the function result.
def evenValues(values) {
return (for let value in values => if value % 2 == 0 => value)
}
Without parentheses, the generator expression is unpacked and the first yielded value becomes the function result.
def firstGreeting(tone) {
return switch tone {
case is Tone.Friendly => 'Hello'
case is Tone.Formal => 'Greetings'
default => 'Hi'
}
}
Important
The return keyword also forces the call to return immediately. All instructions of the function after a reached return will not be executed.
An [[iterator|Built-in-types#iterator]] can also be generated with the yield keyword followed by an expression. The result of the expression is then used as an element of the iterator. When the yield keyword is used, the execution is then temporarily passed to the caller to allow it to use the generated value. The execution returns to the function when the next element of the iterator is requested. This is useful when the computation of a single element of the result takes a lot of time or memory and not all values are needed.
let makeList = def (from, to) {
let value = doSomeBigComputation(from)
while value != to {
yield value
value = doSomeBigComputation(value)
}
}
If the expression is a generator expression wrapped in parentheses, the generated iterator itself is yielded as a single value.
let makeNestedList = def (from, to) {
yield (for let value in from..to => value)
}
Without parentheses, the generator expression is unpacked and all its yielded values are yielded by the function one by one.
let makeList = def (from, to) {
yield for let value in from..to => value
}
Important
Functions that use the yield keyword are called generator functions. This kind of function always returns an iterator even if the yield expression was never reached.
A function call is made using the function call operator on a function instance.
def {} () // call an in-line function
def func {}
func() // call the function referenced by func
The corresponding block of script is selected depending on the number of call arguments given.
let func = def(a) {
return a
}
func += def(a, b) {
return a * b
}
func(10) // gives 10
func(10, 10) // gives 100
The unpack operator can also be used in a function call to expand each element of a container as an individual parameter.
let func = def(a) {
return a
}
func += def(a, b) {
return a * b
}
func(*[10]) // gives 10
func(*[10, 10]) // gives 100
If the function takes at least one parameter, the function call can be made using the . operator of an object. The object will then be used as the first parameter of the function. The function must be defined in the global package or in a package containing the object type for user defined types
def doSum(object) {
for item in object {
if defined result {
result += item
} else {
result = item
}
}
return result
}
[1, 2, 3].doSum() // gives 6
(5).doSum() // gives 5
('f', 'o', 'o').doSum() // gives 'foo'
Warning
For numeric constants, the '()' operator must be used to avoid confusion between the . operator and the decimal separator.
When passing parameters to a function, callback functions can be declared inline using the => operator. This kind of function can only provides a result based on a single expression and capture and call parameters. The => operator replaces the opening { of the function declaration and is followed by a single expression.
let foo = myArray \
.filter(def(i) => i % 2 === 0) \
.transform(def(i) => i * 2) \
.accumulate(1, def(i, j) => i * j)
Mint supports coroutines using the async and await keywords. Coroutines allow writing asynchronous code in a sequential and readable style.
A coroutine is defined using async def.
async def greet(name) {
return "Hello " + name
}
Calling an async function does not immediately execute it. It returns a [[coroutine|Built-in-types#coroutine]] object.
To execute a coroutine, it must be awaited from another coroutine or passed to the runtime (for example using spawn() from the mint.coroutine module).
The await keyword suspends the current coroutine until the awaited coroutine completes.
async def get_value() {
return 42
}
async def main() {
let value = await get_value()
return value + 1
}
When await is reached:
- The current coroutine is suspended.
- The awaited coroutine runs.
- When it finishes, execution resumes after the
await.
Semantically, await behaves like a normal function call. The only difference is that execution may be suspended between call and return.
Mint supports asynchronous generators, which combine the behavior of generators and coroutines. They allow producing a sequence of values over time while using await inside their execution.
An asynchronous generator is defined using async def and yield:
async def produce(self) {
yield 1
yield 2
yield 3
}
Calling an asynchronous generator returns an [[iterator||Built-in-types#async_iterator]]:
let it = spawn(produce())
Values are retrieved using next, which returns a coroutine:
let a = spawn(it.next()) // 1
let b = spawn(it.next()) // 2
let c = spawn(it.next()) // 3
The iterator is exhausted when isEmpty() returns true:
it.isEmpty() // true
Asynchronous generators can suspend on other coroutines:
async def compute(self, x) {
return x * 2
}
async def generate(self) {
let a = await compute(10)
yield a
yield a + 1
}
Asynchronous generators can be used with for loops when awaited:
async def f(self) {
yield 1
yield 2
}
async def g(self) {
for let x in await f() {
yield x + 1
}
}
The await is required because the generator itself is produced asynchronously.