A function is a customizable and reusable block of code that can be invoked where ever needed. Functions change the control flow of the program and are used to create layers of abstraction to avoid duplicating code.
You can create a function using the
fn
keyword.
fn greet {
stdout "Hello Kal!\n".
} func.kal
This example creates a function named
greet
that encapsulates the code block inside
{ }
.
To invoke the created function, use the
:
symbol.
Complete picture:
fn greet {
stdout "Hello Kal!\n".
}
:greet. func.kal
Functions can be invoked before their declaration. Consider invoking
greet
before it is defined.
:greet.
fn greet {
stdout "Hello Kal!\n".
}
func.kal
Return data from a function using the
<-
operator. A function halts its execution at the point of return.
Target operator (
->
) can be used during the function invocation to write the return value to a variable.
fn value {
<- 125 * 4.
}
:value -> x.
stdout x "\n". return.kal
A function can be composed of multiple function calls.
fn add -> a, b {
<- a + b.
}
fn mul -> x, y {
<- x * y.
}
fn calc {
:add 45 55 -> valueA.
:mul valueA 3 -> valueB.
<- valueB.
}
:calc -> value.
stdout "Value = " value "\n". fnCompose.kal
Alternatively, you can convert function invocation into an expression using
$(...)
if you wish to perform additional operations on the return value of a function.
fn value {
<- 125 * 4.
}
var x = $(:value).
stdout x "\n".
var y = $(:value) * 2.
stdout y "\n". returnExpr.kal
The behavior of a function can be customized by passing arguments to a function. A function declaration optionally takes a list of parameters that can be used in the function body.
fn add -> x, y {
<- x + y.
}
:add 45 55 -> sum.
stdout "Sum = " sum "\n". fnArgs.kal
In this case, 45 and 55 are arguments which are assigned to function parameters
x
and
y
.
Variables in the parameter list are separated by commas (
,
).
Argument accepting functions can also be invoked as an expression.
fn add -> x, y {
<- x + y.
}
var sum = $(:add 45 55).
stdout "Sum = " sum "\n". fnArgsExpr.kal
Function expressions can be nested together.
fn add -> a, b {
<- a + b.
}
fn mul -> x, y {
<- x * y.
}
var value = $(:mul $(:add 5 5) $(:add 45 55)).
stdout "Value = " value "\n". nestedFnExpr.kal
A function that invokes itself is called a recursive function. A recursive function must always have a base condition that ends the function execution. An absent base condition may cause the function to execute infinitely. Consider the classic factorial example.
fn fact -> n {
;; base condition
if n <= 1 {
<- 1.
}
;; recursive func call
<- n * $(:fact (n - 1)).
}
:fact 5 -> val.
stdout "5! = " val "\n". factorial.kal
Functions parameters can have default values. That means, if a value is not passed to a parameter, the function assigns a default value to it.
Assign default values to a parameter using the
:
operator after the parameter name.
Let's revisit the greet function:
fn greet -> name: "Kal" {
stdout "Hello " name "!\n".
}
:greet.
:greet "Barry". greet.kal
Notice how during the first invocation of greet no argument was passed. Therefore, it picked up the default value. In the next invocation, an actual argument is passed, it thus overrides the default values of the parameter.
Multiple arguments can have default values. It's not mandatory to override none or all arguments of a function. You can choose to override only the required parameters.
fn add -> x: 10, y: 20 {
<- x + y.
}
;; all default
stdout $(:add) "\n".
;; override x, default y
stdout $(:add 20) "\n".
;; override both x and y
stdout $(:add 45 55) "\n". defArgs.kal
So far, we have written functions that take a fixed number of arguments. Often, you may want to write functions that take an unknown number of arguments.
A variadic function accepts a variadic parameter.
A variadic parameter is a single parameter that encapsulates all the unknown amount of arguments into a single list. The function body can then utilize the arguments from the list as needed.
Variadic parameter is created by prefixing it with
...
.
Let's create a variadic function,
sum
which accepts any number of arguments and returns their sum.
fn sum -> ...nums {
;; argument list
stdout "Args: " nums "\n".
var total = 0.
loop each in nums {
total = total + each.
}
<- total.
}
:sum 1 2 3 -> total.
stdout "Total = " total "\n".
:sum 1 2 3 4 5 -> total.
stdout "Total = " total "\n". variadicSum.kal
You can pair regular parameters along with a variadic parameter.
fn sum -> x, y, ...nums {
stdout "First Sum = " (x + y) "\n".
stdout "Args: " nums "\n".
var total = 0.
loop each in nums {
total = total + each.
}
<- total.
}
:sum 1 2 3 4 5 -> total.
stdout "Total = " total "\n". variadicSumAgain.kal
Here, out of all five arguments passed to the invocation of the
sum
function, positionally, 1 and 2 are assigned to
x
and
y
respectively.
The rest of the arguments are encapsulated inside the
nums
parameters as a list.
The variadic parameter must always be the last parameter in the function parameter list.
In contrast to variadics, the spread operator has another use case. It can be used to expand the elements of a list or dictionary into arguments during a function invocation.
Prefix a list variable or a list literal with
...
to expand its contents into arguments.
fn greet -> time, name {
stdout "Good " time " " name "!\n".
}
var valueList = ["evening", "friend"].
:greet ...valueList. spread.kal
Here, the values
"evening"
and
"friend"
from the values list are expanded as arguments to the
greet
function.
They are assigned to
time
and
name
parameters respectively.
You can also mix regular arguments with spread arguments.
fn add -> x, y, z {
<- x + y + z.
}
var valueList = [25, 35].
:add 15 ...valueList -> a.
:add 15 ...[25, 35] -> b.
stdout "A = " a "\n".
stdout "B = " b "\n". regularAndSpread.kal
Order of elements in the list matters since the arguments are assigned positionally.
Spreading a dictionary allows you to expand arguments explicitly based on the names of the parameters.
Kal matches the parameters and values from the dictionary based on the names of the keys. Here, the order of key-value pairs does not matter.
fn greet -> time, name {
stdout "Good " time " " name "!\n".
}
var valueDict = #(
name -> "friend",
time -> "evening"
).
:greet ...valueDict. spreadDict.kal
Notice how the order of parameters in the parameter list for the greet function does not match with the order of key-value pairs from the values dictionary.
Kal matches parameters with the keys from the dictionary resulting in a successful function invocation.
You can ensure the execution of a function at the end of the scope of current function using
defer
.
In many cases, a function returns early. Every statement after a return is skipped. Critical function invocations may also be skipped.
defer
ensures that function invocation happens whether a function ends normally or returns early.
It follows a First In Last Out approach where the function that is deferred first is executed last.
fn end -> id {
stdout "End: " id "\n".
}
fn main {
defer $(:end 1).
defer $(:end 2).
defer $(:end 3).
stdout "Fn: Main\n".
}
:main. defer.kal
Alternatively, you can invoke functions in First In First Out order by queuing multiple invocations in a single
defer
statement.
fn end -> id {
stdout "End: " id "\n".
}
fn main {
defer $(:end 1)
$(:end 2)
$(:end 3).
stdout "Fn: Main\n".
}
:main. deferFIFO.kal
Functions with deferred function invocations can be composed together.
fn end -> id {
stdout "End: " id "\n".
}
fn testA {
defer $(:end 1).
defer $(:end 2).
stdout "testA here!\n".
<- "testA ends!".
}
fn testB {
defer $(:end 3).
defer $(:end 4).
:testA -> status.
stdout status "\n".
stdout "testB here!\n".
}
fn test {
defer $(:testB).
stdout "Done!\n".
}
:test. composedDefer.kal