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.
inlineremoves the lambda allocation and enablesreified, but increases bytecode at every call site — use it for small, hot, generic helpers.- Mark a lambda parameter
noinlinewhen you need to store it or pass it to a non-inline function. crossinlineforbids 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)| Feature | Benefit | Limit |
|---|---|---|
value class | Type safety at no allocation cost | One underlying property; no inheritance |
operator fun | Natural syntax for domain types | Keep it unsurprising |
infix fun | Readable boolean and pair builders | One parameter only |
@JvmInline | Required for JVM value classes | Boxing 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.Related
Generics, variance and delegation Channels, shared state and advanced concurrency
Last refreshed 2026-09-18.