Lesson 9 of 25 · Functions

Functions

Functions are named pieces of a program that take input and give back a result. They are how you break a program into small parts that each do one job, can be read on their own and can be used again.

Declaring and calling

A function is declared as func name(parameters): result type: and its body ends with end. Every parameter is written as name: type, and ret hands the result back to the caller. A function that gives nothing back simply leaves out the result type.

fn-basics.salam
func line total(price: int, quantity: int): int:
    ret price * quantity
end

func show line(name: str, amount: int):
    println name + ":", amount
end

func main:
    tea := line total(45, 2)
    cake := line total(120, 1)
    show line("tea", tea)
    show line("cake", cake)
    show line("total", tea + cake)
end
Outputtea: 90 cake: 120 total: 210

The order of functions in a file does not matter: main may call a function that is declared further down. A call always needs its parentheses, even when there are no arguments.

Returning early

ret leaves the function at once, wherever it stands. That lets you deal with the bad cases first, each with its own ret, and keep the normal path at the bottom without deep nesting. In a function with no result type, a bare ret just leaves.

fn-early-return.salam
func check username(name: str): str:
    if len(name) < 3:
        ret "too short"
    end
    if len(name) > 12:
        ret "too long"
    end
    if name == "admin":
        ret "reserved"
    end
    ret "ok"
end

func greet(name: str):
    if check username(name) != "ok":
        println "cannot greet", name
        ret
    end
    println "welcome,", name
end

func main:
    println check username("al")
    println check username("administrator")
    println check username("admin")
    greet("mina")
    greet("x")
end
Outputtoo short too long reserved welcome, mina cannot greet x

A function with a result type must reach a ret on every path. If it can fall off its end, the compiler stops with E071 and asks for a final ret.

Default values

A parameter with a default value is optional at the call. Put such parameters after the ones without a default.

fn-params.salam
func welcome(name: str, greeting: str = "Hello"):
    println greeting + ",", name
end

func area(width: int, height: int = 1): int:
    ret width * height
end

func main:
    welcome("Mina")
    welcome("Mina", "Good day")
    println area(4), area(4, 5)
end
OutputHello, Mina Good day, Mina 4 20

A default value must be a constant or built from globals; it cannot refer to another parameter, so func box(w: int, h: int = w) fails with "unknown identifier". Write an overload instead (see below). Salam has no named arguments: the arguments always go in the order of the parameters.

Inferring the result type

Leave the result type out and Salam works it out from the ret statements. This is handy for short helpers; for a function others will call, writing the type out makes it easier to read.

fn-infer.salam
func average(a: f64, b: f64):
    ret (a + b) / 2.0
end

func larger(a: int, b: int):
    if a > b:
        ret a
    end
    ret b
end

func main:
    println average(3.0, 4.0), larger(8, 3)
end
Output3.5 8

Returning several values

A function returns one value. When you need to give back more than one, put them in a small struct and return that. The caller reads the parts by name, which is clearer than remembering an order. Structs have a lesson of their own; here all you need is that the fields are marked pub so that code outside the struct can read them.

fn-results.salam
struct Clock:
    pub hours: int
    pub minutes: int
end

func split minutes(total: int): Clock:
    ret Clock { hours = total / 60, minutes = total % 60 }
end

func main:
    trip := split minutes(135)
    println trip.hours, "h", trip.minutes, "min"
    println split minutes(45)
end
Output2 h 15 min Clock {hours = 0, minutes = 45}

Recursion

A function may call itself. Each call gets its own copy of the parameters, and a recursive function always needs a base case, a condition where it returns without calling itself again. Here that is n < 2.

functions.salam
func fibonacci(n: int): int:
    if n < 2:
        ret n
    end
    ret fibonacci(n - 1) + fibonacci(n - 2)
end

func main:
    repeat 10 in i:
        print fibonacci(i), ""
    end
    println ""
end
Output0 1 1 2 3 5 8 13 21 34

This version of fibonacci is easy to read but slow: every call makes two more, so the work roughly doubles with each step of n. For large inputs, a loop that carries the last two values is far faster. Very deep recursion can also run out of stack, so prefer a loop when the depth grows with the size of the input.

Overloading

Several functions may share a name as long as their parameters differ, either in type or in number. Salam picks the right one from the arguments at compile time.

fn-overload.salam
func double(number: int): int:
    ret number * 2
end

func double(text: str): str:
    ret text + text
end

func double(text: str, separator: str): str:
    ret text + separator + text
end

func main:
    println double(21)
    println double("ha")
    println double("ha", "-")
end
Output42 haha ha-ha

Multi-word names

A function name can be several words, so that a call reads like a sentence. Every word is part of the name, so none of them may be a keyword: func print line or func each item is rejected because of print and each.

fn-multiword.salam
func is weekend(day: int): bool:
    ret day == 6 or day == 7
end

func main:
    println is weekend(6), is weekend(2)
end
Outputtrue false

Reference parameters

Parameters are copied by default, so changing one inside the function is invisible outside it. Write &: after the parameter name and the function works on the caller's own variable and can change it. The variable you pass should be declared mut. A reference parameter also saves copying a large struct on every call.

fn-ref.salam
func increase(number &: int):
    number = number + 1
end

func no_effect(number: int): int:
    ret number + 1
end

func main:
    mut score := 10
    increase(score)
    increase(score)
    println score, no_effect(score), score
end
Output12 13 12

Prefer returning a value when you can: score = next score(score) shows what changes right at the call. Use &: when a function updates something in place, such as a struct or a Vector.

Functions as values

A function can also be handed to another function. The bare name of a named function, without parentheses, is its address in memory, typed i64; that is the form C-style callback registries take. To pass a function to a parameter with a function type such as func (int) int, wrap it in a short anonymous function. The next lesson covers both.

main and the exit code

main is where the program starts, and what it returns is the program's exit code. Falling off its end, or a bare ret, exits with 0, which means success. ret 1 (or any other number) reports a failure to the shell or the script that started the program. Error messages go through printerrln, so they do not mix with the normal output.

fn-main-exit.salam
func enough stock(stock: int, ordered: int): bool:
    if ordered > stock:
        printerrln "not enough stock:", ordered, "ordered,", stock, "left"
        ret false
    end
    ret true
end

func main:
    defer println "order log closed"
    if not enough stock(12, 5):
        ret 1
    end
    println "order accepted"
end
Outputorder accepted order log closed

With 20 ordered and only 12 in stock, the program would print the message to the error stream and exit with 1. The deferred line still runs either way. Calling os.Exit inside main is an error (E109), because it would skip main's defer statements, while ret runs them. The defer lesson explains those.

Rules of thumb

  • Give each function one job and a name that says what it does.
  • Handle the bad cases first, each with an early ret.
  • Return a struct to give back several values.
  • Make sure every path of a function with a result type ends in ret.
  • Report failure from main with ret 1, not with os.Exit.