Functions
Functions are code blocks that can be reused in different parts of the program, can accept input parameters, and return an output value. This helps organize code and make it more readable and maintainable. Additionally, by using functions, it's possible to avoid code repetition and facilitate debugging.
Like variables and objects, functions are also first-class values in JARU, meaning they can be assigned to variables, passed as parameters to other functions, and returned as values from other functions.
A function takes up to 255 parameters, although the stack sets the limit before that (see Stack limits). If a new value is assigned to a parameter inside the function, the variable that was passed doesn't change. Lists, maps, arrays, instances and structs, however, are not copied: if the function changes their contents (for example, with append), the change is also visible outside the function.
Function declaration
To declare a function in JARU, use the keyword func followed by the name you want to give the function, the parameters in parentheses, and end with the keyword end.
func myFunction(parameter1, parameter2)
// function body
end
Function parameters are variables that receive the values passed to the function when it's called. A function can have none or several. Assigning a new value to a parameter only changes the parameter, not the original variable; but if the parameter is a list, map, array, instance or struct, the function works on the same object, and changes to its contents are visible outside:
func change(number, list)
number = 99 // doesn't change the variable outside
list.append(4) // changes the list outside
end
var n = 1
var l = [1, 2, 3]
change(n, l)
println(n) // 1
println(l) // [1, 2, 3, 4]
To return a value from a function, use the keyword return followed by the value to return. If a function doesn't return any value, the return keyword is not necessary.
Actually, in JARU all functions return a value. If return is not used in the function, it will return the value nil.
Function declaration examples
// Function that receives no parameters and returns no value
func print_message()
print("Hello world")
end
// Function that receives parameters and returns a value
func add(num1, num2)
return num1 + num2
end
// Function that receives multiple parameters and returns no value
func print_data(name, age, city)
print("Name: ", name, " Age: ", age, " City: ", city)
end
No default values
Parameters don't take default values. This doesn't compile:
func shoot(x, y, speed = 5)
println(x, y, speed)
end
If you want an optional parameter, pass nil to it and decide inside which value to use:
func shoot(x, y, speed)
if (speed == nil) then
speed = 5
end
println("shot at ", x, ", ", y, " at ", speed)
end
shoot(10, 20, nil) // shot at 10, 20 at 5
shoot(10, 20, 8) // shot at 10, 20 at 8
You can't have two functions with the same name and different parameters either: the second declaration replaces the first. Only class constructors can have several versions (see Constructor overloading).
Calling a function
Calling a function is the process of executing the code contained within that function. In JARU, you can call a function using its name followed by parentheses and, optionally, passing arguments inside the parentheses.
print_data("John", 25, "Madrid")
In this case, the print_data function would receive the arguments "John", 25 and "Madrid" and use them to print a message on screen.
It's also possible to store the value returned by a function in a variable:
var result = add(3, 4)
In this case, the variable result would store the value 7.
Each call has to pass as many arguments as the function has parameters. If you pass more or fewer, the error comes up when the call runs: ERR_INVALID_ARG_NUMBER (Function 'add' expects 2 arguments, but got 1.).
A function can call other functions, and it can also declare functions inside its body: see Nested function declaration.
Before its declaration
A function exists from the moment its func runs, just like a variable from its var. If you call it further up, the program compiles, but it stops when it reaches the call with the error ERR_NAME_NOT_FOUND (Undefined variable 'greet'.):
greet() // ERR_NAME_NOT_FOUND: it doesn't exist yet
func greet()
println("Hello")
end
Inside a function, on the other hand, you can call another one declared further down: what counts is that it exists when the call runs. This way, two functions can call each other:
func isEven(n)
if (n == 0) then
return true
end
return isOdd(n - 1)
end
func isOdd(n)
if (n == 0) then
return false
end
return isEven(n - 1)
end
println(isEven(10)) // true
println(isOdd(7)) // true
Scope
A function sees three kinds of variables:
- Its parameters and its local variables, the ones it declares with
varinside. They only exist while the function runs. - Global variables, the ones declared outside any function and any block. A function can read and change them without declaring anything. They just have to exist when the function runs, even if they're declared further down.
- The variables of the functions that contain it, if it's declared inside another one (see Variable capture).
A local variable with the same name as a global one hides it: inside the function, that name is the local one, and the global one doesn't change.
var lives = 3
func loseLife()
lives = lives - 1 // changes the global
points = points + 10 // so does this, although it's declared further down
end
func test()
var lives = 99 // local: hides the global
println("inside: ", lives)
end
var points = 0
loseLife()
println(lives, " ", points) // 2 10
test() // inside: 99
println(lives) // 2
A variable declared inside a block (if, for, while...) only exists until its end, inside a function too (see Variables and constants).
Using a name that doesn't exist doesn't give an error when compiling, but when running: ERR_NAME_NOT_FOUND (Undefined variable 'double'.). It happens when you assign a variable you haven't declared with var, and when you use a variable from a block outside it:
func calculate(n)
if (n > 0) then
var double = n * 2
end
return double // ERR_NAME_NOT_FOUND: double doesn't exist here anymore
end
Nested function declaration
In JARU, functions can be declared inside other functions:
func parent_function()
func child_function()
println("I am the child function")
end
println("I am the parent function")
child_function()
end
parent_function()
In this example, the parent_function function is the main function that contains child_function inside it. The child_function function can only be called inside parent_function, since it's declared within that function.
It's also possible to pass parameters to the child function and return values from it:
func parent_function()
func child_function(num1, num2)
return num1 + num2
end
var result = child_function(2, 3)
print("The result is: ", result)
end
parent_function()
The console will show: The result is: 5
Anonymous functions (fn lambdas)
A lambda is a function without a name used as a value: you can store it in a variable, put it in a list or map, return it from a function, or pass it directly as a callback. It's written with the fn keyword, the parameter list, a colon and the body ending with end:
fn(parameters): body end
// In a variable
var double = fn(x): return x * 2 end
println(double(21)) // 42
// Inline as a callback: the star use case
use Timer
Timer.every(1000, fn(id, p, u):
println("tick")
end)
// In lists and maps (dispatch tables)
var commands = {"greet": fn(who): println("Hello, ", who) end}
var greet = commands["greet"]
greet("Jon")
println, len, sqrt and the other functions of the Standard library can't be stored in a variable or passed as an argument: var f = len doesn't compile. Module functions can't either: var s = Math.sin stops when it runs. Wrap them in a lambda:
use Math
var sine = fn(x): return Math.sin(x) end
println(sine(0)) // 0
Variable capture (closures)
A lambda can use the variables of the scope where it was created. Capture is by reference: the lambda sees later changes to the variable, and the variable stays alive as long as the lambda exists — even after the function where it was born has returned:
func makeCounter()
var n = 0
return fn():
n = n + 1
return n
end
end
var c1 = makeCounter()
var c2 = makeCounter()
println(c1(), " ", c1(), " ", c1()) // 1 2 3
println(c2()) // 1 (each counter has its own n)
Inside a class method, a lambda can also capture this:
class Button
def init()
this.pressed = 0
end
def handler()
return fn(): this.pressed++ end
end
end
Recursion with lambdas
A lambda has no name, so for recursion assign it to a variable first and call it through it:
var fact = nil
fact = fn(n):
if (n <= 1) then
return 1
end
return n * fact(n - 1)
end
println(fact(5)) // 120
If a function returns another function, you can call the result directly: make()(x). What doesn't work yet is indexing what a call returns, f(x)[0]: it compiles, but at runtime it fails with the error This object type is not indexable. Store the result in a variable first: var r = f(x) then r[0].
Recursion
In JARU, recursion is allowed: a function that calls itself to solve a problem. Recursion is often used to solve problems that can be divided into smaller subproblems of the same type.
To create a recursive function in JARU, you must define the function the same way you define any other function, but within the function, you must include a call to the same function.
func factorial(n)
if (n == 0) then
return 1
else
return n * factorial(n - 1)
end
end
In this example, the factorial function calls itself with a parameter one less until the base case is reached (n == 0).
A recursive function needs a base case that it always reaches. Without one, it calls itself until it fills the VM's stack, and the VM closes. And even with one, every call takes room on the stack: recursion can't go deeper than a few dozen or a few hundred levels (see Stack limits).
Common recursion examples
- Search and sorting algorithms: Algorithms like quicksort and binary search use recursion to divide a problem into smaller problems and solve them independently.
- Backtracking problems: Problems like the rat in a maze and solving sudokus can be solved using recursion to try different options and backtrack when reaching a path with no solution.
- Mathematical problems: Problems like the Fibonacci series and factorial calculation can be efficiently solved using recursion.
- Graph problems: Algorithms like depth-first search and breadth-first search for traversing a graph use recursion to explore different nodes.
Recursion is a powerful tool for solving problems that can be divided into independent subproblems, and JARU allows you to use it.
Stack limits
Each call in progress keeps on the VM's stack the function, its arguments, its local variables and the calculations it has halfway done. That stack has room for 256 values for the whole program, and it's the same size on Windows and on the boards.
That's why recursion has a limit, which depends on what each call keeps. Measured on Windows:
| Each call | Levels |
|---|---|
One parameter and nothing else: down(n + 1) | 127 |
One parameter and an addition halfway done: return n + sum(n - 1) | 85 |
| Three parameters and two local variables | 42 |
The same happens with many arguments: the compiler accepts up to 255 parameters, but a call with 254 arguments no longer fits on the stack.
If a recursion or a chain of calls fills the stack, the VM closes without any message (on Windows; it hasn't been tested on the boards), and try doesn't catch it: the ERR_RECURSION_DEPTH_EXCEEDED constant exists, but the VM doesn't throw it. If a function can call itself more than a few dozen times, write it with a loop.
All the known issues of JARU, together, are in Known issues.
// Recursive: with a big n it runs out of stack
func sumRecursive(n)
if (n == 0) then
return 0
end
return n + sumRecursive(n - 1)
end
// With a loop: works for any n
func sumLoop(n)
var total = 0
for (var i = 1; i <= n; i++)
total = total + i
end
return total
end
println(sumRecursive(50)) // 1275
println(sumLoop(1000)) // 500500