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.
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)
endNever 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.
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)
endThe 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:
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)
endThe 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.
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
endLena 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.
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)
endWhat 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.
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
endTo 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.
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
endNotice 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.
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
endRules of thumb
- Use a lambda for a short piece of behaviour that belongs to one call; give it a name with
funconce 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.