Defining Functions (fn name(x: int) -> str)

Defining Functions (fn name(x: int) -> str)

Functions in Luma let you group logic into reusable, well-typed building blocks. They follow the same principles as the rest of the language:

  • Clear and explicit: parameter and return types are always visible.
  • Minimal but useful: one main way to define functions.
  • Consistent with lambdas: function types use the same (...) -> ... shape.

This page covers:

  • Block-style functions with fn and return
  • Single-expression shortcut using =>
  • Parameters and return types
  • Function types for higher-order functions
  • Optionals in function signatures
  • Common patterns and gotchas

Basic Function Syntax

The simplest function has:

  • A name
  • Zero or more parameters with types
  • A return type
  • A body in { ... } with a return statement
fn greet(name: str) -> str {
    return "Hello, ${name}!"
}
  • fn - starts a function definition
  • greet - function name
  • name: str - parameter name of type str
  • -> str - function returns a str
  • Body - everything inside { ... }, must eventually return a str

Calling the function:

message: str = greet("Luma")
print(message)    // Hello, Luma!

Single-Expression Functions (=>)

For small functions, you can skip the braces and return keyword.

fn add(a: int, b: int) -> int => a + b

This is equivalent to:

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

Another example:

fn greet(name: str) -> str => "Hello, ${name}!"

Rules for => functions:

  • The function body is a single expression after =>.
  • The expression must match the declared return type.
  • You still write the parameter list and -> ReturnType.

Function Types and Higher-Order Functions

Luma supports lambdas (x -> x > 2). Function types reuse the same arrow syntax: (ParamTypes...) -> ReturnType.

Function type syntax

// A function that takes an int and returns an int
doubler: (int) -> int

// A function that takes a str and returns a str
formatter: (str) -> str

You can use these types in:

  • Variable declarations
  • Parameters
  • Return types

Passing a function as a parameter

fn apply_twice(f: (int) -> int, x: int) -> int {
    return f(f(x))
}

fn increment(n: int) -> int => n + 1

result: int = apply_twice(increment, 5)
print(result)        // 7

You can also pass lambdas directly:

result2: int = apply_twice(n -> n * 2, 3)
print(result2)       // 12

Returning a function

fn make_prefixer(prefix: str) -> (str) -> str {
    return name -> "${prefix}${name}"
}

add_hello: (str) -> str = make_prefixer("Hello, ")
print(add_hello("Luma"))   // Hello, Luma

This pattern is useful for building reusable, configured helpers.

Optionals in Function Signatures

Functions can take and return optional types, just like variables.

Optional parameters

fn greet_optional(name: ?str) -> str {
    if name == nil {
        return "Hello, stranger!"
    }
    return "Hello, ${name}"
}

Usage:

n1: ?str = nil
n2: ?str = "Luma"

print(greet_optional(n1))   // Hello, stranger!
print(greet_optional(n2))   // Hello, Luma

Optional return types

Optional returns are great for “maybe found” operations:

fn find_index(nums: [int], target: int) -> ?int {
    nums.walk(i, v) -> {
        if v == target {
            return i
        }
    }
    return nil
}

nums: [int] = [1, 2, 3]
idx: ?int = find_index(nums, 2)

if idx != nil {
    print("Index: ${idx}")
}

Functions, Interpolation, and Collections

Functions work naturally with Luma’s other features:

Interpolation in return values

fn summary(count: int, avg: float) -> str {
    return "Count = ${count}, avg = ${avg}"
}

Working with lists and maps

fn sum(nums: [int]) -> int {
    total: int = 0
    nums.walk(x) -> {
        total = total + x
    }
    return total
}

fn keys_of(m: {str: int}) -> [str] {
    ks: [str] = []
    m.walk(k, v) -> {
        ks.add(k)
    }
    return ks
}

Best Practices

A few guidelines to keep functions readable and “Luma-like”:

  • Keep them small: one clear responsibility per function.
  • Use meaningful names:
    • sum, filter_active, to_message instead of f1, doStuff.
  • Use => only when the implementation is truly a single, simple expression.
  • Prefer explicit return types rather than inference (for now).
  • Use optional return types for “maybe” results instead of magic values like -1.

Gotchas

A few things to watch out for:

  • Missing return

    Every code path in a function must return the declared type.

fn bad(x: int) -> int {
    if x > 0 {
        return x
    }
    // ERROR: no return for x <= 0
}
  • Return type must match

    Returning different types based on branches is not allowed:

fn weird(flag: bool) -> int {
    if flag {
        return 1
    } else {
        return "nope"   // ERROR: str vs int
    }
}
  • Function type must match

    When passing functions as arguments, parameter and return types must align exactly:

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

fn wrong(s: str) -> str => s

apply(wrong, 1)   // ERROR: (str) -> str is not (int) -> int

Quick Reference

Define a function (block)

fn name(param1: Type1, param2: Type2) -> ReturnType {
    // ...
    return value
}

Define a function (single expression)

fn name(param: Type) -> ReturnType => expression

Function type in a variable

op: (int) -> int
op = n -> n * 2

Higher-order function

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

Optional returns

fn find(...args) -> ?int {
    // ...
    return nil      // or: return index
}
Last updated on