Functions as Values
Also known as: first class function higher order function pointer delegate callback rest parameters variadic varargs ...args *args spread any number of arguments
● Beginner5 minBefore this: LambdasSpec 3.4
A function is a value: store it in a variable, or pass it to another function.
fn add(a: Int, b: Int) -> Int {
a + b
}
fn double(x: Int) -> Int {
x * 2
}
fn apply_twice(f: fn(Int) -> Int, x: Int) -> Int {
f(f(x))
}
fn main() {
const op: fn(Int, Int) -> Int = add
print(op(2, 3))
print(apply_twice(double, 5))
print([1, 2, 3].map(double))
}The program ran and printed:
kavach run values.kav
520[2, 4, 6]
fn(Int, Int) -> Int is the type of a function that takes two Ints and gives one. apply_twice takes such a function as its parameter f and calls it.
Functions that can fail
A function type says what its functions can fail with, and what they may use, in the order of a signature: fn(NewOrder) -> Order ! ShopError uses Db. Several capabilities go in parentheses: uses (Net, Db).
type Order {
item: String
qty: Int
}
type ShopError = OutOfStock(item: String) | TooMany(qty: Int)
fn place(o: Order) -> String ! ShopError {
if o.qty > 10 {
fail ShopError.TooMany(qty: o.qty)
}
if o.item == "kite" {
fail ShopError.OutOfStock(item: o.item)
}
"{o.qty} x {o.item}"
}
fn handle<P, R, E>(input: P, handler: fn(P) -> R ! E) -> R ! E {
try handler(input)
}
fn main() {
for o in [Order(item: "pen", qty: 2), Order(item: "kite", qty: 1), Order(item: "cup", qty: 50)] {
const said = handle(o, place) catch e => match e {
ShopError.OutOfStock(item) => "no {item} left",
ShopError.TooMany(qty) => "{qty} is too many",
}
print(said)
}
}The program ran and printed:
kavach run fn-errors.kav
2 x penno kite left50 is too many
handletakes any function that can fail. Its error typeEis a type parameter, sohandlefails withplace's ownShopError, and thematchon it must cover every case.- Calling a function value that can fail is a call that can fail:
try,catchormatch, as for any other call. - A function that can't fail fits any function type, with
!or without. - With
uses, a function value may use only what its type lists; without it, the type sets no limit. See Pure Functions.
A function that can fail doesn't fit a type without !, since its error would be lost:
type ShopError = TooMany(qty: Int)
fn check(qty: Int) -> Int ! ShopError {
if qty > 10 {
fail ShopError.TooMany(qty: qty)
}
qty
}
fn apply(f: fn(Int) -> Int, n: Int) -> Int {
f(n)
}
fn main() {
print(apply(check, 50))
}The compiler refused the program:
kavach run fn-errors.kav
error: `fn(Int) -> Int ! ShopError` can fail, so it doesn't fit `fn(Int) -> Int`: handle the error inside it, or write `! ErrorType` in the function type (spec 3.4)A method can take a function that can fail too, call it, and handle its error. Here using gives the connection back whether work succeeds or fails, then passes the error on with fail e:
type DbError = Busy | Gone(reason: String)
type Pool {
next: Shared<Int>
/// Runs `work` with a connection, given back even when `work` fails.
fn using<T>(self, work: fn(Int) -> T ! DbError) -> T ! DbError {
const conn = self.next.update(n => n + 1)
print("took connection {conn}")
const out = work(conn) catch e => {
print("gave {conn} back")
fail e
}
print("gave {conn} back")
out
}
}
fn count_orders(conn: Int) -> Int ! DbError {
if conn > 1 {
fail DbError.Busy
}
conn * 10
}
fn main() {
const pool = Pool(next: Shared(0))
print(pool.using(count_orders) catch _ => -1)
print(pool.using(count_orders) catch _ => -1)
}The program ran and printed:
kavach run method-fn-errors.kav
took connection 1gave 1 back10took connection 2gave 2 back-1
Any number of values
There are no rest parameters, such as ...args in JavaScript or *args in Python. A function that takes any number of values takes a list, and a call puts them in brackets:
fn total(prices: [Int]) -> Int {
prices.sum(p => p)
}
fn main() {
print(total([4, 8, 15]))
print(total([]))
const cart = [1, 2, 3]
print(total(cart)) // a list passes on as it is
}The program ran and printed:
kavach run many-values.kav
2706
- An empty list is fine:
total([])is 0. - A list you already have passes on as it is. There is no spread operator to learn, such as
...listor*list. - The parameter is like any other, so it mixes with named arguments and defaults without extra rules. Rust, Zig and Haskell do the same.
Values without the brackets are refused:
fn total(prices: [Int]) -> Int {
prices.sum(p => p)
}
fn main() {
print(total(4, 8, 15))
}The compiler refused the program:
kavach run many-values.kav
error: `total` takes one list, `prices`, not separate values: put them in a list, as in `total([…])` (spec 3.1)What Kavach does for you
A stored callback is called directly
A function kept in a record and called later is inlined when the compiler can see which one it is.