Skip to content

Chapter 4: Functions

Why Functions Matter

You could write all your code in main. For small programs, that's fine. But as programs grow, you need to break them into pieces. Functions are those pieces.

Good functions do one thing well. They have clear inputs and outputs. They're easy to test, easy to understand, and easy to reuse. Bad functions do too much, have unclear responsibilities, and make your codebase a nightmare to maintain.

Let's learn to write good ones.

The Basics

A function has a name, parameters, a return type, and a body:

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

fn main() -> int {
    var sum = add(5, 3)
    print(sum)  // 8
    return 0
}

The syntax is straightforward:

  • fn declares a function
  • add is the name
  • (a: int, b: int) are the parameters with their types
  • -> int is the return type
  • The body is in braces

When you call add(5, 3), the values 5 and 3 are copied into a and b. The function runs, returns a value, and that value replaces the function call in the expression.

Parameters: Inputs to Your Function

Parameters are how you pass data into functions:

wyn
fn greet(name: string) -> int {
    print("Hello, ")
    print(name)
    return 0
}

fn main() -> int {
    greet("Alice")
    greet("Bob")
    return 0
}

Each parameter needs a type. The compiler uses these types to check that you're calling the function correctly:

wyn
greet(42)  // Error: expected string, got int

This catches bugs at compile time, before your code runs.

Multiple Parameters

Separate parameters with commas:

wyn
fn calculate_area(width: int, height: int) -> int {
    return width * height
}

fn main() -> int {
    var area = calculate_area(10, 5)
    print(area)  // 50
    return 0
}

Parameter order matters. calculate_area(10, 5) is different from calculate_area(5, 10) if the function treats them differently.

Pass by Value

Wyn passes arguments by value-the function gets a copy:

wyn
fn modify(x: int) -> int {
    x = x + 10  // Modifies the copy
    return x
}

fn main() -> int {
    var original = 5
    var result = modify(original)
    
    print(original)  // 5 - unchanged
    print(result)    // 15 - modified copy
    
    return 0
}

This is safe but can be inefficient for large data structures. We'll address that when we cover references later.

Return Values: Outputs from Your Function

Every function in Wyn must return a value. Even if that value is just 0 to indicate success:

wyn
fn print_greeting(name: string) -> int {
    print("Hello, ")
    print(name)
    return 0
}

The return type comes after ->. It tells the compiler what type of value the function produces:

wyn
fn get_answer() -> int {
    return 42
}

fn get_pi() -> float {
    return 3.14159
}

fn is_valid(x: int) -> bool {
    return x > 0
}

fn get_name() -> string {
    return "Alice"
}

Early Returns

You can return from anywhere in a function:

wyn
fn divide(a: int, b: int) -> int {
    if (b == 0) {
        return 0  // Early return for error case
    }
    return a / b
}

This is often clearer than nesting the main logic in an else block.

Recursion: Functions Calling Themselves

A function can call itself. This is recursion:

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

fn main() -> int {
    print(factorial(5))  // 120
    return 0
}

How does this work? Let's trace it:

factorial(5)
= 5 * factorial(4)
= 5 * (4 * factorial(3))
= 5 * (4 * (3 * factorial(2)))
= 5 * (4 * (3 * (2 * factorial(1))))
= 5 * (4 * (3 * (2 * 1)))
= 120

Each call creates a new stack frame with its own n. When the base case is reached (n <= 1), the recursion unwinds and the results multiply together.

The Danger of Recursion

Recursion is elegant but dangerous. Each call uses stack space. Too many calls and you overflow the stack:

wyn
fn infinite_recursion(n: int) -> int {
    return infinite_recursion(n + 1)  // Never stops!
}

This will crash. Always have a base case that stops the recursion.

Tail Recursion

Some recursive functions can be optimized by the compiler:

wyn
fn factorial_tail(n: int, acc: int) -> int {
    if (n <= 1) {
        return acc
    }
    return factorial_tail(n - 1, n * acc)
}

fn factorial(n: int) -> int {
    return factorial_tail(n, 1)
}

This is tail recursion-the recursive call is the last thing the function does. The compiler can optimize this into a loop, avoiding stack overflow.

In practice, use loops for iteration unless recursion makes the code significantly clearer.

Function Composition: Building Blocks

Functions are building blocks. Combine them to create more complex behavior:

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

fn increment(x: int) -> int {
    return x + 1
}

fn main() -> int {
    var x = 5
    var result = increment(double(x))  // (5 * 2) + 1 = 11
    print(result)
    return 0
}

Read function composition inside-out: double(x) runs first, then increment runs on that result.

Naming Functions: Make Intent Clear

Function names should be verbs or verb phrases that describe what the function does:

wyn
// Good names
fn calculate_total(price: float, tax: float) -> float
fn is_valid_email(email: string) -> bool
fn send_notification(user: string, message: string) -> int

// Bad names
fn calc(x: float, y: float) -> float  // Too abbreviated
fn check(s: string) -> bool           // Too vague
fn do_stuff(a: string, b: string) -> int  // Meaningless

Boolean functions often start with is_, has_, or can_:

wyn
fn is_empty(text: string) -> bool
fn has_permission(user: string) -> bool
fn can_access(resource: string) -> bool

Pure Functions: No Side Effects

A pure function always returns the same output for the same input and has no side effects:

wyn
// Pure: same inputs always give same output
fn add(a: int, b: int) -> int {
    return a + b
}

// Impure: has side effect (prints)
fn add_and_print(a: int, b: int) -> int {
    var sum = a + b
    print(sum)  // Side effect!
    return sum
}

Pure functions are easier to test, easier to reason about, and easier to parallelize. Prefer them when possible.

But don't be dogmatic. Sometimes side effects are necessary. Just be aware of them.

Function Design: One Thing Well

Each function should do one thing. If you can't describe what a function does in a single sentence, it's probably doing too much.

wyn
// Bad: does too much
fn process_user_and_send_email_and_log(name: string, email: string) -> int {
    // Validate user
    // Save to database
    // Send welcome email
    // Log the action
    // Update metrics
    return 0
}

// Good: separate concerns
fn validate_user(name: string, email: string) -> bool
fn save_user(name: string, email: string) -> int
fn send_welcome_email(email: string) -> int
fn log_user_creation(name: string) -> int

Small, focused functions are easier to test, easier to understand, and easier to reuse.

When to Write a Function

Write a function when:

  1. You're repeating code: If you copy-paste code, extract it into a function
  2. Logic is complex: If a block of code is hard to understand, give it a name
  3. You want to test something: Functions are the unit of testing
  4. You want to reuse logic: Functions are the unit of reuse

Don't write a function when:

  1. It's only used once and simple: Sometimes inline code is clearer
  2. It's just a wrapper: Don't write fn add(a, b) { return a + b } unless you have a good reason
  3. It makes code harder to follow: Sometimes abstraction obscures rather than clarifies

Use judgment. There's no hard rule.

What You've Learned

Functions encapsulate logic. They take inputs (parameters), do work, and produce outputs (return values). They're the primary tool for organizing code.

Good functions are small, focused, and well-named. They do one thing well. They're easy to test and easy to understand.

Recursion is powerful but dangerous. Use it when it makes code clearer, but be aware of stack limits.

Pure functions are easier to reason about. Prefer them when possible, but don't be dogmatic.

Exercises

  1. Max of three: Write a function that returns the maximum of three integers. Don't use any built-in max function.

  2. Is palindrome: Write a function that checks if a string is a palindrome (reads the same forwards and backwards). Ignore case and spaces.

  3. Fibonacci iterative: Implement Fibonacci without recursion. Use a loop instead.

  4. Temperature conversion: Write functions to convert between Celsius, Fahrenheit, and Kelvin. Make sure they compose correctly.

  5. Prime checker: Write a function that returns true if a number is prime. Then write a function that returns all primes up to n. The second function should use the first.

  6. GCD: Implement the Euclidean algorithm for finding the greatest common divisor of two numbers. Use recursion.

  7. Refactor: Take a long function (maybe from a previous exercise) and break it into smaller functions. Notice how it becomes easier to understand.


Next: Chapter 5: Structs

Try It Yourself

Install Wyn and run this example:

bash
curl -sSL wynlang.com/install.sh | sh
wyn init hello && cd hello
wyn run src/main.wyn

MIT License - v1.19.1