Higher-Order Functions & Function Types
Reviewed & published by Brayan K
A higher-order function is a function that takes another function as a parameter or returns one, treating functions as ordinary values you can pass around, store, and call later.
Part of the free Kotlin course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn function types like (Int) -> Int, how to pass and return functions, function references with ::name, trailing-lambda syntax, and the implicit it parameter.
What You'll Learn in This Lesson
- What a function type like (Int, Int) -> Int means
- Storing functions in variables and calling them
- Passing a function into another function
- Returning a function from a function
- Function references with the :: operator
- Trailing-lambda syntax and the implicit it
1️⃣ Function Types: Functions as Values
In Kotlin, a function has a type just like an Int or a String does. The type is written as (parameterTypes) -> returnType. For example, (Int, Int) -> Int describes any function that takes two Ints and returns an Int. Once a function has a type, you can store it in a variable and call it through that variable.
fun main() {
// A function TYPE describes a function's shape: (parameters) -> returnType.
// Here is a variable that holds a function taking two Ints and returning an Int:
val add: (Int, Int) -> Int = { a, b -> a + b }
val multiply: (Int, Int) -> Int = { a, b -> a * b }
// Because they share the same type, you can call them the same way:
println(add(2, 3)) // -> 5
println(multiply(2, 3)) // -> 6
// A function with no parameters that returns a String:
val greeting: () -> String = { "Hello" }
println(greeting()) // -> Hello
// A function that takes a String and returns nothing (Unit):
val shout: (String) -> Unit = { text -> println(text.uppercase()) }
shout("hi") // -> HI
}Notice that add and multiply share the same type, so they are interchangeable as values. A function returning nothing useful has the return type Unit — Kotlin's equivalent of "void".
2️⃣ Passing Functions In (Higher-Order Functions)
A higher-order function accepts a function as a parameter. The parameter is declared with a function type, and inside the body you call it like any other function. When the function parameter is the last argument, Kotlin lets you write the lambda outside the parentheses — the idiomatic trailing-lambda syntax.
// A higher-order function: it ACCEPTS another function as a parameter.
// The parameter 'op' has the function type (Int, Int) -> Int.
fun combine(a: Int, b: Int, op: (Int, Int) -> Int): Int {
return op(a, b)
}
fun main() {
// Pass a lambda directly as the third argument:
println(combine(10, 4, { x, y -> x + y })) // -> 14
// When the lambda is the LAST argument, move it outside the parentheses.
// This is "trailing-lambda" syntax and is the idiomatic Kotlin style:
println(combine(10, 4) { x, y -> x - y }) // -> 6
// With exactly one parameter, Kotlin gives you the implicit name 'it'.
val timesTen: (Int) -> Int = { it * 10 }
println(timesTen(5)) // -> 50
}When a lambda has exactly one parameter, you can omit it and use the implicit name it. This is why list.filter { it > 0 } reads so cleanly.
3️⃣ Returning Functions & Function References
A higher-order function can also return a function — useful for building configured behaviour like a "multiply by N" factory. And when you already have a named function, the reference operator :: turns it into a value you can pass around, so you don't need to wrap it in a lambda.
// A higher-order function can also RETURN a function.
// multiplierFor returns a brand-new function that multiplies by 'factor':
fun multiplierFor(factor: Int): (Int) -> Int {
return { n -> n * factor }
}
// Function references: :: turns an existing named function into a value.
fun isEven(n: Int): Boolean = n % 2 == 0
fun main() {
val triple = multiplierFor(3)
println(triple(4)) // -> 12
val double = multiplierFor(2)
println(double(4)) // -> 8
// Pass the named function by reference with ::isEven (no parentheses):
val numbers = listOf(1, 2, 3, 4, 5, 6)
val evens = numbers.filter(::isEven)
println(evens) // -> [2, 4, 6]
}Each call to multiplierFor produces a fresh function that "remembers" its factor — a closure. And numbers.filter(::isEven) is cleaner than numbers.filter { isEven(it) }.
Your turn. Fill in the ___, then run and compare.
// 🎯 YOUR TURN — replace each ___ then run main()
fun applyTwice(x: Int, op: (Int) -> Int): Int {
// Apply op to x, then apply op again to that result:
return op(op(x))
}
fun main() {
// 1) Pass a trailing lambda that adds 3 to its input.
// Use the implicit single-parameter name here:
val result = applyTwice(10) { ___ + 3 } // 👉 put "it" before " + 3"
println(result)
// ✅ Expected: 16 (10 + 3 = 13, then 13 + 3 = 16)
}Mini-Challenge: A Tiny Calculator
Write one higher-order function and reuse it with three different lambdas.
// 🎯 MINI-CHALLENGE: build a tiny calculator that takes an operation.
// Save as Calc.kt and run main().
//
// 1. Write a function calc(a: Int, b: Int, op: (Int, Int) -> Int): Int
// that returns op(a, b).
// 2. Call it three times using trailing-lambda syntax:
// - addition (a + b)
// - subtraction (a - b)
// - the larger of the two (use if/else inside the lambda)
// with a = 8 and b = 3, printing each result.
//
// ✅ Expected output:
// 11
// 5
// 8
fun main() {
// your code here
}Common Errors (and the fix)
- ❌ "Type mismatch: expected (Int) -> Int" — Your lambda's parameter or return type doesn't match the declared function type. ✅ Make sure the lambda takes and returns the same types the function type specifies.
- ❌ Calling a function value without parentheses — Writing add alone gives you the function itself, not its result. ✅ Call it with add(2, 3).
- ❌ Putting () after a reference — ::isEven() is wrong. ✅ A reference is written without parentheses: ::isEven.
- ❌ Using it with two parameters — it only exists for single-parameter lambdas. ✅ Name them: { a, b -> a + b }.
Pro Tips
- 💡 Prefer references: .filter(::isEven) is often clearer than .filter { isEven(it) }.
- 💡 Trailing lambdas read like blocks: repeat(3) { ... } and list.forEach { ... } are both higher-order calls.
- 💡 Name long lambda params: for anything beyond a one-liner, explicit names beat it for readability.
📋 Quick Reference — Higher-Order Functions
| Concept | Kotlin Syntax |
|---|---|
| Function type | (Int, Int) -> Int |
| Lambda value | { a, b -> a + b } |
| Accept a function | fun f(op: (Int) -> Int) |
| Trailing lambda | f(10) { it + 1 } |
| Function reference | list.filter(::isEven) |
| Implicit parameter | { it * 10 } |
🎉 Lesson Complete!
- ✅ A function type (Int, Int) -> Int describes a function's shape
- ✅ Functions are values: store them, pass them, return them
- ✅ A higher-order function takes or returns a function
- ✅ Trailing-lambda syntax moves the last lambda outside the parentheses
- ✅ ::name references a named function; it names a single lambda parameter
Practice quiz
What makes a function 'higher-order'?
- It returns an Int
- It takes a function as a parameter or returns one
- It is marked inline
- It has more than three parameters
Answer: It takes a function as a parameter or returns one. A higher-order function takes another function as a parameter, returns one, or both.
What does the function type (Int, Int) -> Int describe?
- A class with two Int fields
- A list of two Ints
- A function taking two Ints and returning an Int
- An Int divided by an Int
Answer: A function taking two Ints and returning an Int. It describes a function taking two Int parameters and returning an Int.
What is the return type of a function that returns nothing useful?
- Unit
- Void
- Null
- Nothing
Answer: Unit. Kotlin uses Unit as the type for functions that return no useful value.
In a single-parameter lambda, what is the implicit parameter name?
- this
- arg
- self
- it
Answer: it. A single-parameter lambda can refer to its argument with the implicit name it.
When is trailing-lambda syntax allowed?
- When the lambda is the last argument
- Only with zero other arguments
- When the lambda is the first argument
- Never in Kotlin
Answer: When the lambda is the last argument. If the function's last parameter is a function, the lambda can move outside the parentheses.
How do you reference an existing named function isEven as a value?
- isEven()
- ::isEven
- &isEven
- isEven.ref
Answer: ::isEven. The :: operator turns a named function into a value, e.g. ::isEven (no parentheses).
What does multiplierFor(3) return in the lesson example?
- The number 3
- A list of multiples
- A new function that multiplies by 3
- An error
Answer: A new function that multiplies by 3. It returns a new function (Int) -> Int that multiplies its input by 3.
What lets a returned lambda remember the factor variable?
- A closure capturing the variable
- A global variable
- The inline keyword
- Garbage collection
Answer: A closure capturing the variable. The lambda forms a closure, capturing factor even after the outer function returns.
Why is ::isEven written without parentheses?
- Parentheses are optional everywhere
- It references the function instead of calling it
- It is a syntax error to omit them
- It only works for Boolean functions
Answer: It references the function instead of calling it. A reference names the function as a value; adding () would call it instead.
Can 'it' be used in a lambda with two parameters?
- Yes, it refers to the first
- Yes, it refers to both
- No, name the parameters explicitly
- Yes, but only inside loops
Answer: No, name the parameters explicitly. it exists only for single-parameter lambdas; with two, name them like { a, b -> ... }.
Continue this course
- Previous: Checkpoint: Kotlin Basics
- Next: Inline Functions & reified — Avoid lambda allocation and access generic types at runtime with reified
- Quick reference: Kotlin cheat sheet
Frequently asked questions
What exactly makes a function 'higher-order'?
A higher-order function is simply a function that takes another function as a parameter, returns a function, or both. The functions it works with are ordinary values — you can store them in variables, pass them around, and call them later. This is what powers Kotlin's collection operations like filter, map, and forEach: each of them accepts a function describing what to do, which is far more flexible than writing a separate loop every time.
What is the difference between a function type and a lambda?
A function type, like (Int, Int) -> Int, is the type annotation that describes a function's shape: what parameters it takes and what it returns. A lambda, like { a, b -> a + b }, is one way to create a value of that type — a compact, unnamed function literal. You can also satisfy a function type with a function reference such as ::isEven. The type is the contract; the lambda or reference is a concrete implementation of it.
When should I use trailing-lambda syntax?
Whenever the function's last parameter is itself a function, Kotlin lets you move that lambda outside the parentheses. So combine(10, 4) { x, y -> x + y } means the same thing as combine(10, 4, { x, y -> x + y }). It is the idiomatic, readable style and is the reason constructs like list.filter { it > 0 } and repeat(3) { ... } look so clean. If the lambda is the only argument, you can drop the parentheses entirely.
What is the 'it' keyword inside a lambda?
When a lambda has exactly one parameter, you can skip declaring it and refer to that single argument with the implicit name it. So { it * 10 } is shorthand for { n -> n * 10 }. It keeps short lambdas concise, but if a lambda has two or more parameters, or if nesting makes it ambiguous, name the parameters explicitly for clarity.