Skip to content
Kavachकवच

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]
values.kav: Kavach code, then what running it prints

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
fn-errors.kav: Kavach code, then what running it prints
  • handle takes any function that can fail. Its error type E is a type parameter, so handle fails with place's own ShopError, and the match on it must cover every case.
  • Calling a function value that can fail is a call that can fail: try, catch or match, 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)
fn-errors.kav: Kavach code, then what running it prints

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
method-fn-errors.kav: Kavach code, then what running it prints

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
many-values.kav: Kavach code, then what running it prints
  • 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 ...list or *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)
many-values.kav: Kavach code, then what running it prints

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.

    devirt.rs