Kotlin Crash Course
Chapter 2

Functions

Declaring functions, default arguments, named parameters, lambdas, and higher-order functions. Functions are first-class citizens in Kotlin.

By the end of this chapter, you will:

  • Declare functions with parameters and return types
  • Use default arguments and named parameters
  • Write single-expression functions
  • Create and pass lambda expressions
  • Understand higher-order functions and the it keyword

Declaring Functions

Functions in Kotlin start with the fun keyword. The parameter type comes after the parameter name, separated by a colon.

// Function with parameters and return type
fun greet(name: String): String {
    return "Hello, $name!"
}

// Function with no return value (returns Unit, like void)
fun printGreeting(name: String) {
    println("Hello, $name!")
}

// Calling functions
val message = greet("Alex")
println(message)  // Hello, Alex!
Important: Function parameters are implicitly val — you cannot reassign them. This trips up JavaScript and Python developers who are used to mutating parameters.
fun doubleIt(n: Int): Int {
    // n = n * 2   // ❌ ERROR: Val cannot be reassigned
    return n * 2   // ✅ Create a new value instead
}
// Function with receiver type — extends existing classes
fun String.addExclamation(): String = this + "!"
val excited = "Hello".addExclamation()  // "Hello!"

// Local function — scoped inside another function
fun factorial(n: Int): Int {
    fun helper(x: Int, acc: Int): Int =
        if (x <= 1) acc else helper(x - 1, acc * x)
    return helper(n, 1)
}
// Function returning a function — creates a multiplier
fun makeMultiplier(factor: Int): (Int) -> Int {
    return { number -> number * factor }
}
val triple = makeMultiplier(3)
println(triple(4))  // 12

// Closure modifying captured variable
var sum = 0
listOf(1, 2, 3).forEach { sum += it }
println(sum)  // 6

Compare with JS / Python

JavaScript
function doubleIt(n) {
    n = n * 2;   // Allowed (but doesn't affect caller)
    return n;
}
Python
def double_it(n):
    n = n * 2    # Allowed (rebinds local name)
    return n

Kotlin difference: Kotlin prevents parameter reassignment at compile time. This avoids bugs where you accidentally mutate a parameter thinking it affects the caller. It's also a form of defensive programming — parameters are inputs, not local variables.

Compare with JS / Python

JavaScript
function greet(name) {
    return `Hello, ${name}!`;
}

const greet = (name) => `Hello, ${name}!`;
Python
def greet(name: str) -> str:
    return f"Hello, {name}!"

Kotlin difference: Parameter types are required and come after the name (name: String). Return type comes after parameters (): String). No implicit return.

Default Arguments & Named Parameters

This is one of Kotlin's best features — inherited from Python but with static type safety.

fun createUser(
    name: String,
    age: Int = 0,              // default value
    isActive: Boolean = true   // default value
) {
    println("$name, $age, active=$isActive")
}

// All of these are valid:
createUser("Alex")                          // Alex, 0, active=true
createUser("Alex", 25)                      // Alex, 25, active=true
createUser("Alex", isActive = false)        // Alex, 0, active=false
createUser(name = "Alex", age = 25)         // named parameters — order doesn't matter!
Why this matters: Named parameters let you skip optional arguments without passing dummy values. This eliminates the "boolean trap" you see in many APIs: sendEmail(to, from, true, false, null) becomes sendEmail(to, from, encrypt = true).

Compare with JS / Python

JavaScript
function createUser(name, age = 0, isActive = true) {
    console.log(name, age, isActive);
}

createUser("Alex");
createUser("Alex", undefined, false);  // ugly!
Python
def create_user(name, age=0, is_active=True):
    print(name, age, is_active)

create_user("Alex")
create_user("Alex", is_active=False)  # named args

Kotlin difference: Very similar to Python! But Kotlin checks types at compile time, so you get safety + convenience.

Single-Expression Functions

When a function body is a single expression, you can omit the curly braces and use =.

// Instead of this:
fun add(a: Int, b: Int): Int {
    return a + b
}

// Write this:
fun add(a: Int, b: Int): Int = a + b

// Return type can even be inferred:
fun multiply(a: Int, b: Int) = a * b
Tip: Single-expression functions are idiomatic in Kotlin. Use them for simple getters, math operations, and any function that just returns a value.

Lambdas & Higher-Order Functions

Functions are first-class in Kotlin. You can pass them as arguments, return them, and store them in variables.

Lambda Syntax

// A lambda (anonymous function)
val square: (Int) -> Int = { x -> x * x }

// If there's only one parameter, it's called `it` automatically
val double: (Int) -> Int = { it * 2 }

// Using lambdas
println(square(5))    // 25
println(double(5))    // 10

Higher-Order Functions

A higher-order function is a function that takes another function as a parameter or returns one.

// Takes a function as parameter
fun operateOn(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

// Usage
val sum = operateOn(3, 4) { x, y -> x + y }     // 7
val product = operateOn(3, 4) { x, y -> x * y } // 12

// Trailing lambda syntax: if lambda is last arg, put it outside parentheses
val max = operateOn(3, 4) { x, y ->
    if (x > y) x else y
}

More Lambda Patterns

Kotlin lambdas support destructuring, function references, and anonymous functions for cases where you need explicit return types.

// Destructuring in lambdas — iterate over maps cleanly
val headers = mapOf("Content-Type" to "application/json", "Auth" to "Bearer token")
headers.forEach { (key, value) ->
    println("$key: $value")
}

// Anonymous function — useful when you need explicit return type
val numbers = listOf(1, 2, 3, 4, 5)
val odds = numbers.filter(fun(n: Int): Boolean {
    return n % 2 != 0
})

// Function references — pass existing functions instead of lambdas
fun isOdd(n: Int) = n % 2 != 0
val oddsRef = numbers.filter(::isOdd)           // reference to top-level function
val toIntRef = listOf("1", "2", "3").map(String::toInt)  // reference to member function
When to use each:
{ } — short inline functions
fun() { } — multiple return points or explicit return types
::name — when you already have a function that does exactly what you need

Compare with JS / Python

JavaScript
const square = x => x * x;

function operateOn(a, b, operation) {
    return operation(a, b);
}

const sum = operateOn(3, 4, (x, y) => x + y);
Python
square = lambda x: x * x

def operate_on(a, b, operation):
    return operation(a, b)

sum_val = operate_on(3, 4, lambda x, y: x + y)

Kotlin difference: The it convention and trailing lambda syntax make Kotlin lambdas especially clean. The type system ensures your lambda matches the expected signature at compile time.

Vararg Parameters

Accept a variable number of arguments with vararg (like *args in Python or ...args in JS).

fun sumAll(vararg numbers: Int): Int {
    return numbers.sum()
}

sumAll(1, 2, 3)           // 6
sumAll(1, 2, 3, 4, 5)     // 15

// Spread operator to pass an array
val nums = intArrayOf(1, 2, 3)
sumAll(*nums)             // 6

Local Functions

Kotlin lets you declare functions inside other functions. This keeps helper logic private and organized. Local functions can also capture variables from the outer scope — this is called a closure.

fun validateUser(user: User): Boolean {
    // Local helper function — only visible inside validateUser
    fun isEmailValid(email: String): Boolean {
        return email.contains("@") && email.contains(".")
    }

    fun isAgeValid(age: Int): Boolean {
        return age in 13..120
    }

    return isEmailValid(user.email) && isAgeValid(user.age)
}
// Closure: local function captures outer variable
fun processScores(scores: List): Map> {
    val passingGrade = 60  // outer variable

    // Local function captures `passingGrade` from the enclosing scope
    fun categorize(score: Int): String {
        return if (score >= passingGrade) "pass" else "fail"
    }

    return scores.groupBy { categorize(it) }
}

// processScores(listOf(85, 42, 73, 55))
// → {pass=[85, 73], fail=[42, 55]}
Why use local functions: They keep helper logic private to the function that needs it — no risk of other code calling your internal helpers. When a local function captures an outer variable, you don't need to pass it as a parameter, keeping the call site clean.

Generic Functions

Generic functions work with any type while maintaining type safety. They're essential for writing reusable utilities — in fact, most of Kotlin's standard library functions like filter, map, and listOf are generic.

// Generic function — works with any type T
fun  asList(vararg ts: T): List {
    val result = ArrayList()
    for (t in ts) result.add(t)
    return result
}

val stringList = asList("a", "b", "c")   // List
val intList = asList(1, 2, 3)            // List
// Type constraints — T must be Comparable
fun > sort(list: List): List {
    return list.sorted()
}

// Multiple constraints using where clause
fun  findMax(list: List): T
    where T : Comparable {
    return list.maxOrNull() ?: throw IllegalArgumentException("Empty list")
}
When do you need generics? When you're writing a function that should work the same way regardless of type — like a sorting function that works for Int, String, or any Comparable. You've already been using generics: List<String>, map { it * 2 }, and filter { it > 0 } are all generic under the hood.

Tail Recursion

Deep recursion can cause a StackOverflowError. Kotlin's tailrec modifier optimizes recursive calls into loops, eliminating stack growth.

// Without tailrec: each call adds a stack frame
fun factorial(n: Int): Int {
    return if (n <= 1) 1 else n * factorial(n - 1)
}

// With tailrec: compiler converts recursion to a loop
// The recursive call must be the LAST operation (tail position)
tailrec fun factorial(n: Int, acc: Int = 1): Int {
    return if (n <= 1) acc else factorial(n - 1, n * acc)
}
// Real-world: processing nested data structures safely
tailrec fun flattenPaths(current: String, remaining: List, acc: List = emptyList()): List {
    return when {
        remaining.isEmpty() -> acc
        else -> {
            val next = current + "/" + remaining.first()
            flattenPaths(next, remaining.drop(1), acc + next)
        }
    }
}

// This won't crash even with thousands of nested items
val paths = flattenPaths("/home", listOf("user", "docs", "projects", "kotlin"))
Requirements: The recursive call must be the very last operation in the function. If you do anything after the recursive call (like n * factorial(n - 1)), the compiler can't optimize it and will show a warning.
When to use: Use tailrec for algorithms that naturally express as recursion but may process deep data — tree traversal, path resolution, state machines, and mathematical sequences. In functional programming styles, tail recursion replaces loops while keeping the code declarative.

Infix Functions

Infix functions let you call a function without dots or parentheses, making code read more like natural language or mathematical notation.

// Define an infix extension function
infix fun Int.times(str: String) = str.repeat(this)

// Call it like an operator
val result = 2 times "Bye "    // "Bye Bye "

// Another example: range check
infix fun Int.isMultipleOf(n: Int) = this % n == 0

val check = 10 isMultipleOf 5   // true
// Real-world: building DSL-like APIs for tests or configuration
infix fun String.shouldBe(expected: String) {
    if (this != expected) throw AssertionError("Expected '$expected' but was '$this'")
}

"Hello" shouldBe "Hello"   // reads like English!

// Kotlin standard library uses infix extensively
val range = 1..10           // .. is an infix function
val map = mapOf("a" to 1)   // to is an infix function creating a Pair
Requirements: Infix functions must be member functions or extension functions. They must have exactly one parameter. The parameter cannot accept a variable number of arguments and cannot have a default value.
When to use:
• DSLs and testing frameworks
• When a function represents a natural relationship between two values (like to for pairs)
• Kotlin's to and until are built-in infix examples

Don't overuse — infix calls can hurt readability if the operation isn't intuitive.

Try It Yourself

Write a function formatPrice that takes a Double and an optional currency parameter (default "$"), and returns a formatted string like "$19.99" or "€19.99".

// Your code here:




// Should work:
// println(formatPrice(19.99))       // $19.99
// println(formatPrice(19.99, "€"))  // €19.99
Stuck? Show hint
fun formatPrice(price: Double, currency: String = "$") = "$currency$price" — but this shows all decimals. Try String.format("%s%.2f", currency, price) for 2 decimal places.

Concept Check

What does this code print?

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

println(greet("Alex", greeting = "Hi"))
Hello, Alex!
Hi, Alex!
It won't compile — Kotlin doesn't support named parameters