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:
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:
fndeclares a functionaddis the name(a: int, b: int)are the parameters with their types-> intis 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:
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:
greet(42) // Error: expected string, got intThis catches bugs at compile time, before your code runs.
Multiple Parameters
Separate parameters with commas:
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:
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:
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:
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:
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:
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)))
= 120Each 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:
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:
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:
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:
// 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 // MeaninglessBoolean functions often start with is_, has_, or can_:
fn is_empty(text: string) -> bool
fn has_permission(user: string) -> bool
fn can_access(resource: string) -> boolPure Functions: No Side Effects
A pure function always returns the same output for the same input and has no side effects:
// 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.
// 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) -> intSmall, focused functions are easier to test, easier to understand, and easier to reuse.
When to Write a Function
Write a function when:
- You're repeating code: If you copy-paste code, extract it into a function
- Logic is complex: If a block of code is hard to understand, give it a name
- You want to test something: Functions are the unit of testing
- You want to reuse logic: Functions are the unit of reuse
Don't write a function when:
- It's only used once and simple: Sometimes inline code is clearer
- It's just a wrapper: Don't write
fn add(a, b) { return a + b }unless you have a good reason - 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
Max of three: Write a function that returns the maximum of three integers. Don't use any built-in max function.
Is palindrome: Write a function that checks if a string is a palindrome (reads the same forwards and backwards). Ignore case and spaces.
Fibonacci iterative: Implement Fibonacci without recursion. Use a loop instead.
Temperature conversion: Write functions to convert between Celsius, Fahrenheit, and Kelvin. Make sure they compose correctly.
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.
GCD: Implement the Euclidean algorithm for finding the greatest common divisor of two numbers. Use recursion.
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:
curl -sSL wynlang.com/install.sh | sh
wyn init hello && cd hello
wyn run src/main.wyn