Functional Kotlin: lambdas, inline functions and DSLs

Function types and receivers, higher-order functions, inline and noinline, value classes, and building a small type-safe DSL.

Function types and receivers

// a plain function type
val double: (Int) -> Int = { it * 2 }

// a function type with a receiver: Builder.() -> Unit
class HtmlBuilder {
    private val parts = mutableListOf<String>()
    fun text(value: String) { parts += value }
    fun render() = parts.joinToString("")
}

fun html(block: HtmlBuilder.() -> Unit): String {
    val builder = HtmlBuilder()
    builder.block()                  // the lambda body runs with builder as this
    return builder.render()
}

val page = html {
    text("<h1>Reports</h1>")
    text("<p>Generated on demand.</p>")
}

// last parameter lambda syntax
listOf(1, 2, 3)
    .filter { it % 2 == 1 }
    .map { it * it }
    .also { println(it) }

// inline keeps the lambda out of the call stack and avoids an allocation
inline fun <T> measure(tag: String, block: () -> T): T {
    val start = System.nanoTime()
    try {
        return block()
    } finally {
        println(tag + " took " + (System.nanoTime() - start) / 1_000_000 + " ms")
    }
}
  • A trailing lambda is only inferred when it is the last parameter, which is why DSL functions put the block last.
  • inline removes the lambda allocation and enables reified, but increases bytecode at every call site — use it for small, hot, generic helpers.
  • Mark a lambda parameter noinline when you need to store it or pass it to a non-inline function.
  • crossinline forbids a non-local return, which matters when the lambda runs in another context such as a callback.

Value classes and operator conventions

@JvmInline
value class UserId(val raw: String)

@JvmInline
value class Money(val pence: Long) {
    operator fun plus(other: Money) = Money(pence + other.pence)
    operator fun times(factor: Int) = Money(pence * factor)
    override fun toString() = "GBP " + pence / 100.0
}

fun charge(id: UserId, amount: Money) = println("charging " + id.raw + " " + amount)

// the compiler erases the wrapper at runtime in most positions,
// so this compiles to a plain String plus Long
charge(UserId("u-1"), Money(1250) * 2)
FeatureBenefitLimit
value classType safety at no allocation costOne underlying property; no inheritance
operator funNatural syntax for domain typesKeep it unsurprising
infix funReadable boolean and pair buildersOne parameter only
@JvmInlineRequired for JVM value classesBoxing when used as a nullable or generic type

A small type-safe DSL

@DslMarker
annotation class RouteDsl

@RouteDsl
class RouteBuilder(private val prefix: String) {
    private val children = mutableListOf<String>()

    fun get(path: String, handler: (String) -> String) {
        children += "GET " + prefix + path + " -> " + handler(prefix + path)
    }

    fun post(path: String, handler: (String) -> String) {
        children += "POST " + prefix + path + " -> " + handler(prefix + path)
    }

    fun build() = children.toList()
}

fun routes(prefix: String, block: RouteBuilder.() -> Unit): List<String> =
    RouteBuilder(prefix).apply(block).build()

val defined = routes("/api") {
    get("/health") { "ok" }
    post("/orders") { "created " + it }
}
⚠️
Use @DslMarker on every builder receiver. Without it, an inner block can silently call an outer builder's method, and a typo produces a route in the wrong place instead of a compile error.

FAQ

Should I make every helper inline?
No. Inline expands the body at each call site, which grows the bytecode and can slow the compiler. Inline when you need a non-local return, a reified type parameter, or you have measured that the lambda allocation matters.
What is the difference between <code>apply</code>, <code>also</code>, <code>let</code> and <code>run</code>?
apply configures an object and returns it, also performs a side effect and returns the receiver, let transforms the value and gives you it, and run executes a block with the receiver as this and returns the block result.

Generics, variance and delegation Channels, shared state and advanced concurrency

Last refreshed 2026-09-18.