Kotlin - Functions

Kotlin Functions

Functions are the building blocks of Kotlin programs. Learn how to declare, call, and use functions effectively with parameters, return types, and Kotlin's unique features.

Basic Function Declaration

Simple Function

fun greet() {
    println("Hello, World!")
}

fun main() {
    greet()  // Call the function
}

Function with Parameters

fun greet(name: String) {
    println("Hello, $name!")
}

fun main() {
    greet("Alice")    // Hello, Alice!
    greet("Bob")      // Hello, Bob!
}

Function with Return Value

fun add(a: Int, b: Int): Int {
    return a + b
}

fun main() {
    val result = add(5, 3)
    println("5 + 3 = $result")  // 5 + 3 = 8
}
Beginner Note: The basic syntax is fun functionName(parameters): ReturnType { body }. If there's no return value, you can omit the return type (it defaults to Unit).

Function Syntax Breakdown

fun calculateArea(width: Double, height: Double): Double {
    return width * height
}

// Breakdown:
// fun          - keyword to declare function
// calculateArea - function name (camelCase)
// (width: Double, height: Double) - parameters with types
// : Double     - return type
// { }          - function body

Return Types

Functions with Different Return Types

fun getName(): String {
    return "Alice"
}

fun getAge(): Int {
    return 25
}

fun getHeight(): Double {
    return 5.8
}

fun isStudent(): Boolean {
    return true
}

fun printMessage(): Unit {  // Unit is optional
    println("Message printed")
}

// Unit return type can be omitted
fun printMessage2() {
    println("Message printed")
}

Single Expression Functions

For simple functions, you can use the = syntax:

// Traditional way
fun add(a: Int, b: Int): Int {
    return a + b
}

// Single expression function
fun add(a: Int, b: Int): Int = a + b

// With type inference (return type can be omitted)
fun add(a: Int, b: Int) = a + b

// More examples
fun square(x: Int) = x * x
fun isEven(n: Int) = n % 2 == 0
fun greeting(name: String) = "Hello, $name!"

Function Parameters

Multiple Parameters

fun calculateBMI(weight: Double, height: Double): Double {
    return weight / (height * height)
}

fun displayPerson(name: String, age: Int, city: String) {
    println("Name: $name, Age: $age, City: $city")
}

fun main() {
    val bmi = calculateBMI(70.0, 1.75)
    displayPerson("Alice", 30, "New York")
}

Default Parameters

fun greet(name: String, greeting: String = "Hello") {
    println("$greeting, $name!")
}

fun createUser(name: String, age: Int = 18, isActive: Boolean = true) {
    println("User: $name, Age: $age, Active: $isActive")
}

fun main() {
    greet("Alice")                    // Hello, Alice!
    greet("Bob", "Hi")               // Hi, Bob!
    
    createUser("Charlie")            // Uses default age and isActive
    createUser("Diana", 25)          // Uses default isActive
    createUser("Eve", 30, false)     // All parameters specified
}

Named Arguments

fun createAccount(username: String, email: String, age: Int, isVerified: Boolean = false) {
    println("Account: $username, $email, $age, verified: $isVerified")
}

fun main() {
    // Positional arguments
    createAccount("alice", "[email protected]", 25, true)
    
    // Named arguments (can be in any order)
    createAccount(
        email = "[email protected]",
        username = "bob", 
        age = 30,
        isVerified = true
    )
    
    // Mix of positional and named
    createAccount("charlie", email = "[email protected]", age = 28)
}
Teacher Note: Named arguments are especially useful when functions have many parameters or when some parameters might be unclear from their position.

Variable Number of Arguments (vararg)

fun sum(vararg numbers: Int): Int {
    var total = 0
    for (number in numbers) {
        total += number
    }
    return total
}

fun printItems(vararg items: String) {
    for (item in items) {
        println("- $item")
    }
}

fun main() {
    println(sum(1, 2, 3))           // 6
    println(sum(10, 20, 30, 40))    // 100
    
    printItems("Apple", "Banana", "Cherry")
    
    // Spread operator with arrays
    val numbers = intArrayOf(1, 2, 3, 4, 5)
    println(sum(*numbers))  // * spreads the array
}

Local Functions

Functions can be declared inside other functions:

fun processData(data: List<String>) {
    
    fun validateItem(item: String): Boolean {
        return item.isNotBlank() && item.length > 2
    }
    
    fun formatItem(item: String): String {
        return item.trim().lowercase()
    }
    
    val validItems = data.filter { validateItem(it) }
    val formattedItems = validItems.map { formatItem(it) }
    
    println("Processed items: $formattedItems")
}

fun main() {
    processData(listOf("Apple", "  BANANA  ", "x", "Cherry"))
}

Function Types and Higher-Order Functions

Functions as Parameters

fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

fun add(x: Int, y: Int) = x + y
fun multiply(x: Int, y: Int) = x * y

fun main() {
    println(calculate(5, 3, ::add))      // 8 (using function reference)
    println(calculate(5, 3, ::multiply)) // 15
    
    // Using lambda expressions
    println(calculate(5, 3) { x, y -> x - y })  // 2
    println(calculate(5, 3) { x, y -> x / y })  // 1
}

Functions Returning Functions

fun createMultiplier(factor: Int): (Int) -> Int {
    return { number -> number * factor }
}

fun main() {
    val double = createMultiplier(2)
    val triple = createMultiplier(3)
    
    println(double(5))  // 10
    println(triple(4))  // 12
}

Extension Functions

Add functionality to existing classes without modifying them:

// Extend String class
fun String.isPalindrome(): Boolean {
    val cleaned = this.lowercase().replace(" ", "")
    return cleaned == cleaned.reversed()
}

// Extend Int class  
fun Int.isEven() = this % 2 == 0

// Extend List class
fun <T> List<T>.secondOrNull(): T? {
    return if (this.size >= 2) this[1] else null
}

fun main() {
    println("racecar".isPalindrome())  // true
    println("hello".isPalindrome())    // false
    
    println(4.isEven())   // true
    println(7.isEven())   // false
    
    val numbers = listOf(10, 20, 30)
    println(numbers.secondOrNull())  // 20
}
Architecture Note: Extension functions are a powerful way to add functionality to existing APIs without modifying the original code. They're resolved statically, not through inheritance.

Infix Functions

Functions that can be called with infix notation:

infix fun Int.pow(exponent: Int): Int {
    var result = 1
    repeat(exponent) {
        result *= this
    }
    return result
}

infix fun String.concat(other: String): String {
    return this + other
}

fun main() {
    // Regular function call
    println(2.pow(3))     // 8
    
    // Infix notation (no dots or parentheses)
    println(2 pow 3)      // 8
    println("Hello" concat " World")  // Hello World
    
    // Built-in infix functions
    val map = mapOf(1 to "one", 2 to "two")  // 'to' is infix
}

Tail Recursive Functions

Optimize recursive functions to prevent stack overflow:

// Regular recursion (can cause stack overflow)
fun factorial(n: Int): Long {
    return if (n <= 1) 1 else n * factorial(n - 1)
}

// Tail recursive version
tailrec fun factorialTailRec(n: Int, accumulator: Long = 1): Long {
    return if (n <= 1) accumulator else factorialTailRec(n - 1, n * accumulator)
}

tailrec fun findElement(list: List<Int>, element: Int, index: Int = 0): Int {
    return when {
        index >= list.size -> -1
        list[index] == element -> index
        else -> findElement(list, element, index + 1)
    }
}

fun main() {
    println(factorialTailRec(10))  // 3628800
    println(findElement(listOf(1, 3, 5, 7, 9), 5))  // 2
}

Function Overloading

Multiple functions with the same name but different parameters:

fun print(message: String) {
    println("String: $message")
}

fun print(number: Int) {
    println("Number: $number")
}

fun print(flag: Boolean) {
    println("Boolean: $flag")
}

fun print(items: List<String>) {
    println("List: ${items.joinToString(", ")}")
}

fun main() {
    print("Hello")              // String: Hello
    print(42)                   // Number: 42
    print(true)                 // Boolean: true
    print(listOf("A", "B"))     // List: A, B
}

Common Patterns and Best Practices

Input Validation

fun divide(a: Double, b: Double): Double {
    require(b != 0.0) { "Division by zero is not allowed" }
    return a / b
}

fun createUser(name: String, age: Int): String {
    require(name.isNotBlank()) { "Name cannot be blank" }
    require(age >= 0) { "Age cannot be negative" }
    
    return "User($name, $age)"
}

fun main() {
    try {
        println(divide(10.0, 2.0))  // 5.0
        println(divide(10.0, 0.0))  // Exception
    } catch (e: IllegalArgumentException) {
        println("Error: ${e.message}")
    }
}

Function Documentation

/**
 * Calculates the distance between two points
 * @param x1 X coordinate of first point
 * @param y1 Y coordinate of first point  
 * @param x2 X coordinate of second point
 * @param y2 Y coordinate of second point
 * @return The distance as a Double
 */
fun calculateDistance(x1: Double, y1: Double, x2: Double, y2: Double): Double {
    val dx = x2 - x1
    val dy = y2 - y1
    return kotlin.math.sqrt(dx * dx + dy * dy)
}

Early Returns

fun processUser(user: User?): String {
    if (user == null) return "No user provided"
    if (user.name.isBlank()) return "Invalid user name"
    if (user.age < 0) return "Invalid age"
    
    return "Processing user: ${user.name}"
}

fun findFirst(numbers: List<Int>, condition: (Int) -> Boolean): Int? {
    for (number in numbers) {
        if (condition(number)) return number
    }
    return null
}
Best Practice: Use meaningful function names, keep functions small and focused on a single task, and validate input parameters when necessary.

Practice Exercises

  1. Write a function that calculates the area of a circle given its radius
  2. Create a function with default parameters for creating a user profile
  3. Implement a function that takes a variable number of strings and returns the longest one
  4. Write an extension function for String that counts vowels
  5. Create a tail recursive function to calculate Fibonacci numbers

Real-World Example

// Shopping cart example
data class Item(val name: String, val price: Double, val quantity: Int)

fun calculateTotal(items: List<Item>, tax: Double = 0.08): Double {
    val subtotal = items.sumOf { it.price * it.quantity }
    return subtotal * (1 + tax)
}

fun applyDiscount(total: Double, discountPercent: Double = 0.0): Double {
    require(discountPercent in 0.0..100.0) { "Discount must be between 0 and 100" }
    return total * (1 - discountPercent / 100)
}

fun formatCurrency(amount: Double): String = "$%.2f".format(amount)

// Extension function for better readability
fun List<Item>.totalCost(tax: Double = 0.08, discount: Double = 0.0): String {
    val total = calculateTotal(this, tax)
    val finalAmount = applyDiscount(total, discount)
    return formatCurrency(finalAmount)
}

fun main() {
    val cart = listOf(
        Item("Laptop", 999.99, 1),
        Item("Mouse", 25.99, 2),
        Item("Keyboard", 79.99, 1)
    )
    
    println("Total: ${cart.totalCost(tax = 0.10, discount = 5.0)}")
}

Quick Quiz

  1. What keyword is used to declare a function in Kotlin?
  2. How do you create a single expression function?
  3. What does the vararg keyword do?
  4. What is the default return type if none is specified?
Show answers
  1. fun
  2. Use = instead of curly braces: fun add(a: Int, b: Int) = a + b
  3. vararg allows a function to accept a variable number of arguments
  4. Unit (equivalent to void in Java)