Functions

On this page 22

Functions are the building blocks of Home programs. They support type inference, default parameters, generics, and more.

Function Definition

Basic function syntax:

fn greet(name: string) {
  print("Hello, {name}!")
}

fn add(a: int, b: int): int {
  return a + b
}

Return Types

Return types can be explicit or inferred:

// Explicit return type
fn multiply(a: int, b: int): int {
  return a * b
}

// Inferred return type
fn divide(a: float, b: float) {
  return a / b  // Return type inferred as float
}

// No return value (void)
fn log*message(msg: string): void {
  print("[LOG] {msg}")
}

// Implicit void
fn say*hello() {
  print("Hello!")
}

Parameters

Required Parameters

All parameters are required by default:

fn create*user(name: string, age: int, email: string): User {
  return User { name, age, email }
}

// Must provide all arguments
let user = create*user("Alice", 30, "alice@example.com")

Default Parameters

Parameters can have default values:

fn greet(name: string = "World") {
  print("Hello, {name}!")
}

greet()           // Hello, World!
greet("Alice")    // Hello, Alice!

fn create*user(
  name: string,
  age: int = 0,
  active: bool = true
): User {
  return User { name, age, active }
}

let user1 = create*user("Bob")              // age=0, active=true
let user2 = create*user("Charlie", 25)      // active=true
let user3 = create*user("Dave", 30, false)  // all specified

Named Arguments

Call functions with named arguments for clarity:

fn create*rect(x: int, y: int, width: int, height: int): Rect {
  return Rect { x, y, width, height }
}

// Positional arguments
let r1 = create*rect(10, 20, 100, 50)

// Named arguments
let r2 = create*rect(x: 10, y: 20, width: 100, height: 50)

// Named arguments in any order
let r3 = create*rect(width: 100, height: 50, x: 10, y: 20)

// Mix positional and named
let r4 = create*rect(10, 20, width: 100, height: 50)

Named-Only Parameters

Use * to require named arguments:

fn connect(
  host: string,
  port: int = 8080,
  *,  // Everything after this must be named
  timeout: int = 30,
  retries: int = 3,
  ssl: bool = false
): Connection {
  // ...
}

// These work:
let c1 = connect("localhost")
let c2 = connect("localhost", 3000)
let c3 = connect("localhost", timeout: 60, ssl: true)

// This fails (timeout must be named):
// let c4 = connect("localhost", 3000, 60)

Generic Functions

Functions can be parameterized over types:

fn identity<T>(value: T): T {
  return value
}

let x = identity(42)        // T = int
let s = identity("hello")   // T = string

fn swap<T>(a: T, b: T): (T, T) {
  return (b, a)
}

let (x, y) = swap(1, 2)     // (2, 1)

Multiple Type Parameters

fn map<T, U>(items: []T, f: fn(T): U): []U {
  let result = []U.init(items.len)
  for (i, item in items) {
    result[i] = f(item)
  }
  return result
}

let numbers = [1, 2, 3, 4, 5]
let doubled = map(numbers, |x| x * 2)  // [2, 4, 6, 8, 10]
let strings = map(numbers, |x| "{x}")  // ["1", "2", "3", "4", "5"]

Trait Bounds

Constrain generic types with traits:

fn print*all<T: Display>(items: []T) {
  for (item in items) {
    print("{item}")
  }
}

fn max<T: Ord>(a: T, b: T): T {
  if (a > b) { a } else { b }
}

// Multiple bounds
fn process<T: Clone + Debug>(item: T) {
  let copy = item.clone()
  print("{:?}", copy)
}

Higher-Order Functions

Functions can take functions as parameters:

fn apply(x: int, f: fn(int): int): int {
  return f(x)
}

fn double(x: int): int {
  return x * 2
}

let result = apply(5, double)  // 10

Returning Functions

fn make*adder(x: int): fn(int): int {
  return |y| x + y
}

let add*5 = make*adder(5)
let add*10 = make*adder(10)

print(add*5(3))   // 8
print(add*10(3))  // 13

Closures

Anonymous functions that capture their environment:

// Basic closure
let double = |x| x * 2
let sum = |a, b| a + b

// With type annotations
let multiply = |a: int, b: int|: int {
  a * b
}

// Multi-line closure
let complex = |x| {
  let y = x * 2
  let z = y + 1
  z
}

Capturing Variables

Closures capture variables from their enclosing scope:

let multiplier = 3
let multiply = |x| x * multiplier  // Captures 'multiplier'

print(multiply(5))  // 15

Mutable Captures

let mut count = 0

let increment = || {
  count += 1
  print("Count: {count}")
}

increment()  // Count: 1
increment()  // Count: 2
increment()  // Count: 3

Move Closures

Use move to transfer ownership into the closure:

let data = [1, 2, 3, 4, 5]
let name = "Numbers"

let consume = move || {
  print("{name}: {data}")
  let sum = data.iter().sum()
  print("Sum: {sum}")
}

consume()
// data and name are no longer accessible here

Method Syntax

Functions can be defined as methods on structs:

struct Point {
  x: int,
  y: int
}

impl Point {
  // Associated function (no self)
  fn origin(): Point {
    Point { x: 0, y: 0 }
  }

  // Method (takes self)
  fn distance*from*origin(self): float {
    ((self.x * self.x + self.y * self.y) as float).sqrt()
  }

  // Mutable method
  fn translate(mut self, dx: int, dy: int) {
    self.x += dx
    self.y += dy
  }
}

let p = Point.origin()
let dist = p.distance*from*origin()

Recursion

Functions can call themselves:

fn factorial(n: int): int {
  if (n <= 1) {
    return 1
  }
  return n * factorial(n - 1)
}

print(factorial(5))  // 120

Tail Recursion

Home optimizes tail-recursive functions:

fn factorial*tail(n: int, acc: int = 1): int {
  if (n <= 1) {
    return acc
  }
  return factorial*tail(n - 1, n * acc)  // Tail position
}

Function Composition

Combine functions to create new ones:

fn compose<F, G, A, B, C>(f: F, g: G): fn(A): C
where
  F: Fn(B): C,
  G: Fn(A): B
{
  move |x| f(g(x))
}

let add*one = |x| x + 1
let double = |x| x * 2

let add*then*double = compose(double, add*one)
print(add*then*double(5))  // 12 = (5 + 1) * 2

Async Functions

See the Async Programming guide for details on async functions:

fn fetch*data(): async Result<Data> {
  let response = await http.get("/api/data")
  return response.json()
}

Next Steps

Released under the MIT License.