In the previous tutorial, you learned about Flow. Now let’s learn about inline functions, reified types, and contracts. These are advanced Kotlin features that help you write faster code, work with generic types at runtime, and give the compiler extra information about your functions.

In this tutorial, you will learn:

  • Inline functions and why they matter
  • Non-local returns
  • noinline and crossinline
  • Reified type parameters
  • Contracts (callsInPlace, returns)
  • Practical examples

What Are Inline Functions?

When you pass a lambda to a function, Kotlin creates a lambda object behind the scenes. This has a small performance cost: memory allocation and method calls.

An inline function is copied directly at the call site. No lambda object is created. The compiler replaces the function call with the actual code.

// Regular function — creates a lambda object
fun measureTimeNormal(block: () -> Unit): Long {
    val start = System.nanoTime()
    block()
    return System.nanoTime() - start
}

// Inline function — no lambda object
inline fun measureTimeInline(block: () -> Unit): Long {
    val start = System.nanoTime()
    block()
    return System.nanoTime() - start
}

Both do the same thing, but the inline version is faster because the compiler copies the code directly:

// What you write
val time = measureTimeInline {
    doSomething()
}

// What the compiler generates (simplified)
val start = System.nanoTime()
doSomething()
val time = System.nanoTime() - start

When to Use Inline

Use inline when:

  • The function takes lambda parameters
  • The function is called frequently (hot paths)
  • You need non-local returns or reified types

Do not use inline when:

  • The function body is very large (increases code size)
  • The function does not take lambda parameters (no benefit)
  • The function is rarely called (overhead is negligible)

Non-Local Returns

Inline functions enable non-local returns. This means a return inside the lambda returns from the enclosing function, not just the lambda.

inline fun findFirst(list: List<Int>, predicate: (Int) -> Boolean): Int? {
    for (item in list) {
        if (predicate(item)) return item // Returns from findFirst
    }
    return null
}

fun main() {
    val result = findFirst(listOf(1, 2, 3, 4, 5)) { it > 3 }
    println(result) // 4
}

Without inline, the return would only return from the lambda. With inline, it returns from findFirst. This is because the lambda code is inlined directly into the function body.

Non-Local Return from Enclosing Function

You can even return from the calling function using a non-local return:

inline fun repeatAction(times: Int, action: (Int) -> Unit) {
    for (i in 0 until times) {
        action(i)
    }
}

fun processItems() {
    repeatAction(10) { i ->
        if (i == 5) return // Returns from processItems, not just the lambda
        println("Processing $i")
    }
    println("This never prints")
}

noinline

Sometimes you have multiple lambda parameters, but you need to store one of them. You cannot store an inline lambda. Use noinline to prevent a specific parameter from being inlined.

inline fun inlineWithStorage(
    action: () -> Unit,            // This IS inlined
    noinline callback: () -> Unit  // This is NOT inlined
): () -> Unit {
    action()
    return callback // Can return it because it's not inlined
}

You need noinline when you:

  • Store the lambda in a variable
  • Return the lambda from the function
  • Pass the lambda to a non-inline function

crossinline

crossinline prevents non-local returns in a lambda. Use it when the lambda is called from a different execution context (like a thread or callback).

inline fun runInThread(crossinline action: () -> Unit) {
    val thread = Thread {
        action() // Cannot use non-local return here
    }
    thread.start()
    thread.join()
}

Without crossinline, the compiler would allow a return inside action, which would try to return from the enclosing function. But since the lambda runs on a different thread, this would not make sense.

inline vs noinline vs crossinline

ModifierInlined?Non-local return?Can be stored?
(default)YesYesNo
noinlineNoNoYes
crossinlineYesNoNo

Reified Type Parameters

In Java and Kotlin, generic types are erased at runtime. This means List<String> and List<Int> are the same at runtime — just List. You cannot check value is T or get T::class.

Reified type parameters fix this. They preserve type information at runtime. They only work with inline functions.

// This does NOT compile — type is erased
fun <T> isType(value: Any): Boolean {
    return value is T // Error: Cannot check for erased type
}

// This WORKS — reified preserves the type
inline fun <reified T> isType(value: Any): Boolean {
    return value is T // Works!
}

Type Checking

inline fun <reified T> isType(value: Any): Boolean = value is T

println(isType<String>("hello")) // true
println(isType<Int>("hello"))    // false
println(isType<Int>(42))         // true

Getting Type Name

inline fun <reified T> typeName(): String {
    return T::class.simpleName ?: "Unknown"
}

println(typeName<String>()) // "String"
println(typeName<Int>())    // "Int"

Filtering by Type

inline fun <reified T> List<Any>.filterByType(): List<T> {
    return this.filterIsInstance<T>()
}

val mixed: List<Any> = listOf(1, "hello", 2.0, "world", 3)
val strings: List<String> = mixed.filterByType() // ["hello", "world"]
val ints: List<Int> = mixed.filterByType()       // [1, 3]

Default Values by Type

inline fun <reified T> defaultValue(): T? {
    return when (T::class) {
        Int::class -> 0 as T
        String::class -> "" as T
        Boolean::class -> false as T
        Double::class -> 0.0 as T
        else -> null
    }
}

println(defaultValue<Int>())     // 0
println(defaultValue<String>())  // ""
println(defaultValue<Boolean>()) // false

String Parsing

inline fun <reified T> parseString(value: String): T? {
    return when (T::class) {
        Int::class -> value.toIntOrNull() as? T
        Double::class -> value.toDoubleOrNull() as? T
        Boolean::class -> value.toBooleanStrictOrNull() as? T
        String::class -> value as T
        else -> null
    }
}

println(parseString<Int>("42"))      // 42
println(parseString<Double>("3.14")) // 3.14
println(parseString<Int>("abc"))     // null

Contracts

Contracts tell the compiler about the behavior of a function. This helps the compiler with smart casting and variable initialization.

Contracts are experimental but widely used in the standard library. Functions like require(), check(), and run() all use contracts.

callsInPlace

callsInPlace tells the compiler how many times a lambda is called.

@OptIn(ExperimentalContracts::class)
inline fun <T> runOnce(block: () -> T): T {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return block()
}

With this contract, the compiler knows the lambda runs exactly once. This allows variable initialization:

val value: Int
runOnce {
    value = 42 // Compiler allows this because it runs exactly once
}
println(value) // 42

Without the contract, the compiler would complain that value might not be initialized.

InvocationKind Options

KindMeaning
EXACTLY_ONCECalled exactly once
AT_MOST_ONCECalled zero or one times
AT_LEAST_ONCECalled one or more times
UNKNOWNCalled any number of times

returns Contract

The returns contract tells the compiler what happens when a function returns a specific value. This helps with smart casting.

@OptIn(ExperimentalContracts::class)
fun requireNotEmpty(value: String?): Boolean {
    contract {
        returns(true) implies (value != null)
    }
    return !value.isNullOrEmpty()
}

Now the compiler can smart-cast after calling this function:

val name: String? = "Alex"
if (requireNotEmpty(name)) {
    println(name.length) // Smart cast — compiler knows name is not null
}

Without the contract, you would need name!!.length or an explicit null check.

Practical Example: Retry Function

A zero-overhead retry function using inline:

inline fun <T> retry(times: Int, block: (attempt: Int) -> T): T {
    var lastException: Exception? = null
    repeat(times) { attempt ->
        try {
            return block(attempt)
        } catch (e: Exception) {
            lastException = e
        }
    }
    throw lastException ?: IllegalStateException("Retry failed")
}

// Usage
val result = retry(3) { attempt ->
    if (attempt < 2) throw RuntimeException("fail")
    "success"
}
println(result) // "success"

Because the function is inline, the try-catch and loop are copied directly at the call site. No lambda object, no extra overhead.

Practical Example: Resource Management

Like use() or try-with-resources in Java:

inline fun <T> withResource(resource: String, block: (String) -> T): T {
    println("Opening resource: $resource")
    try {
        return block(resource)
    } finally {
        println("Closing resource: $resource")
    }
}

// Usage
val data = withResource("database.db") { resource ->
    "data from $resource"
}

The finally block always runs, even if an exception is thrown. The resource is always closed.

Practical Example: Logging Wrapper

inline fun <T> logged(tag: String, block: () -> T): T {
    println("[$tag] Start")
    val result = block()
    println("[$tag] End: $result")
    return result
}

val sum = logged("calculation") {
    (1..100).sum()
}
// [calculation] Start
// [calculation] End: 5050

Practical Example: Conditional Apply

inline fun <T> T.applyIf(condition: Boolean, block: T.() -> T): T {
    return if (condition) block() else this
}

val text = "hello"
    .applyIf(true) { uppercase() }    // "HELLO"
    .applyIf(false) { reversed() }    // Still "HELLO" — condition is false

This is a useful utility for building chains where some steps are conditional.

Standard Library Inline Functions

Many Kotlin standard library functions use inline and reified types. Here are the most common ones:

FunctionUses InlineUses ReifiedPurpose
let, run, also, applyYesNoScope functions
repeatYesNoRepeat an action
require, checkYesNoPreconditions (with contracts)
buildStringYesNoBuild strings
filterIsInstance<T>()YesYesFilter by type
emptyList<T>()NoYesCreate typed empty list
lazy { }YesNoLazy initialization
synchronizedYesNoThread synchronization

When you call listOf(1, "hello").filterIsInstance<String>(), the reified type parameter lets Kotlin check types at runtime without reflection.

The Cost of Inline

Inline is not free. Every call site gets a copy of the function body. For small functions (scope functions, single expressions), this is fine. For large functions, it bloats the compiled code.

The Kotlin compiler warns you if you use inline on a function without lambda parameters:

// Warning: Expected performance impact from inlining is insignificant
inline fun add(a: Int, b: Int) = a + b

Only use inline when you have lambda parameters, need non-local returns, or need reified types.

Common Mistakes

Mistake 1: Inlining Large Functions

// BAD — large function body gets copied everywhere
inline fun processData(data: List<Int>, transform: (Int) -> Int): List<Int> {
    // 50 lines of complex logic...
    return data.map(transform)
}

// GOOD — keep inline functions small
inline fun processData(data: List<Int>, transform: (Int) -> Int): List<Int> {
    return data.map(transform)
}

Mistake 2: Forgetting Reified Requires Inline

// Does NOT compile
fun <reified T> check(value: Any) = value is T

// Must be inline
inline fun <reified T> check(value: Any) = value is T

Mistake 3: Not Using Contracts for Initialization

// Without contract — compiler error
fun initialize(block: () -> Unit) { block() }
val x: Int
initialize { x = 5 } // Error: val might not be initialized

// With contract — works fine
@OptIn(ExperimentalContracts::class)
inline fun initialize(block: () -> Unit) {
    contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) }
    block()
}
val x: Int
initialize { x = 5 } // OK

Summary

ConceptDescription
inlineCopies function body at call site (no lambda object)
Non-local returnreturn exits the enclosing function
noinlinePrevents inlining of a lambda parameter
crossinlinePrevents non-local returns in a lambda
reifiedPreserves generic type information at runtime
contractTells the compiler about function behavior
callsInPlaceHow many times a lambda is called
returns impliesSmart casting based on return value

Source Code

You can find the source code for this tutorial on GitHub: tutorial-20-inline-reified

What’s Next?

In the next tutorial, you will learn about Kotlin DSLs — how to write expressive, type-safe APIs using lambdas with receivers.