Lesson 10 of 25 · Functions

Anonymous functions

In Salam functions are values too: you can build one without a name, keep it in a variable, hand it to another function or get one back as a result. That is how you tell a sort how to order things or a loop what to do with each item.

An anonymous function

(parameters): body end builds a function with no name, often called a lambda. The body is an ordinary block, so a single ret fits on one line and a longer body spreads over several lines and closes with end. You call it like any other function.

lambda.salam
func apply(f: func (int) int, value: int): int:
    ret f(value)
end

func main:
    double := (n: int): ret n * 2 end
    println double(5)
    println apply((n: int): ret n * n end, 7)
    grade := (score: int):
        passed := score >= 10
        ret passed ? "pass" : "fail"
    end
    println grade(14), grade(8)
end
Output10 49 pass fail

Never write a result type on a lambda: (x: int): int: ret x end does not parse. Salam works the type out from the ret statements. A lambda that returns nothing simply has no ret with a value.

The function type

The type of a function is written func (parameter types) result type. func (int) bool takes one int and gives back a bool; func (int) takes an int and gives back nothing. Use such a type for a parameter, and the function accepts any compatible function. When a type is long or used often, give it a name with type.

lambda-type.salam
type Rule = func (int) bool

func count where(values: int[], keep: Rule): int:
    mut n := 0
    each v in values:
        if keep(v):
            n += 1
        end
    end
    ret n
end

func main:
    temps := [12, 18, 25, 31, 9, 27]
    hot := (t: int): ret t >= 25 end
    println "hot days:", count where(temps, hot)
    println "cold days:", count where(temps, (t: int): ret t < 10 end)
end
Outputhot days: 3 cold days: 1

The rule can be a lambda kept in a variable, like hot, or written straight into the call. Either way count where does not know or care what the rule checks.

Passing a named function

The bare name of a named function is its address, typed i64, not a value of a function type, so apply all(prices, with tax) is rejected with E012, "cannot pass 'i64' to 'func(i32) i32'". Wrap the function in a lambda that calls it:

lambda-named.salam
func with tax(price: int): int:
    ret price + price * 9 / 100
end

func apply all(prices: int[], change: func (int) int):
    each i, p in prices:
        println i, p, "->", change(p)
    end
end

func main:
    apply all([100, 250], (p: int): ret with tax(p) end)
end
Output0 100 -> 109 1 250 -> 272

The i64 form exists for C-style callback slots, such as the routes of a web server. Inside Salam code, the lambda wrapper is the way to go.

Sorting with a comparator

A comparator is a function that takes two items and answers "should the first come before the second?". sort.SortBy sorts a Vector with it, so you decide the order: by name, by score, highest first, or anything else. sort.StableSortBy also keeps items that compare equal in the order they already had.

lambda-sort.salam
import sort

struct Player:
    pub name: str
    pub score: int
end

func main:
    mut team := Vector {} as Vector<Player>
    defer team.free()
    team.push(Player { name = "Sara", score = 72 })
    team.push(Player { name = "Omid", score = 91 })
    team.push(Player { name = "Lena", score = 85 })
    team.push(Player { name = "Arash", score = 85 })
    sort.SortBy(team, (a: Player, b: Player): ret a.name < b.name end)
    each p in team:
        print p.name, ""
    end
    println ""
    sort.StableSortBy(team, (a: Player, b: Player): ret a.score > b.score end)
    each p in team:
        println p.name, p.score
    end
end
OutputArash Lena Omid Sara Omid 91 Arash 85 Lena 85 Sara 72

Lena and Arash both have 85. Because the second sort is stable, they stay in the alphabetical order the first sort left them in. The collections lesson covers Vector, and the sort package has the full list of sorting functions.

Returning a function

A function may build a lambda and return it. The lambda keeps a copy of the values it uses, so every call to the outer function produces a different function. This is a neat way to make a family of related functions from one recipe.

lambda-make.salam
func discount(percent: int): func (int) int:
    ret (price: int): ret price - price * percent / 100 end
end

func main:
    member := discount(10)
    sale := discount(25)
    println member(200), sale(200)
    println member(80), sale(80)
end
Output180 150 72 60

What a lambda captures

A lambda copies the outside variables it uses at the moment it is created. Changing the variable afterwards does not reach the lambda, and a lambda that changes a captured variable changes only its own copy. That copy lives as long as the lambda, so a lambda can count across calls, but the caller never sees the count.

lambda-capture.salam
func main:
    mut limit := 3
    show := (): ret limit end
    limit = 10
    println "limit is", limit, "but the lambda sees", show()
    mut clicks := 0
    click := ():
        clicks += 1
        ret clicks
    end
    println click(), click(), click()
    println "clicks outside:", clicks
end
Outputlimit is 10 but the lambda sees 3 1 2 3 clicks outside: 0

To let a lambda change something outside it, use a mut global, as below, or have the lambda return the new value and assign it at the call.

lambda-share.salam
mut total := 0

func visit all(values: int[], visit: func (int)):
    each v in values:
        visit(v)
    end
end

func main:
    visit all([120, 80, 45], (p: int):
        total += p
    end)
    println "total:", total
end
Outputtotal: 245

Notice that a lambda can be the last argument of a call and still span several lines; the call's closing parenthesis comes right after its end.

Keeping functions in a Vector

A function type can be the element type of a Vector, so a list of steps can be built at run time and then applied in order. Here each step changes the value and passes it on to the next.

lambda-pipeline.salam
func main:
    mut steps := Vector {} as Vector<func (int) int>
    defer steps.free()
    steps.push((x: int): ret x * 2 end)
    steps.push((x: int): ret x + 10 end)
    steps.push((x: int): ret x / 3 end)
    mut value := 7
    each step in steps:
        value = step(value)
        println value
    end
end
Output14 24 8

Rules of thumb

  • Use a lambda for a short piece of behaviour that belongs to one call; give it a name with func once it grows or is needed in several places.
  • Name long function types with type.
  • To pass a named function, wrap it: (p: int): ret with tax(p) end.
  • A lambda works on copies. Share state through a return value or a global.
  • Every time a lambda that captures variables is created, it allocates room for its copies, and that memory is not freed. Create such a lambda once, outside a loop, rather than in every round.