Polyglot
Languages Kotlin extensions
Kotlin § extensions

Extensions

Extension functions and extension properties admit adding members to existing types without modifying the original. The principal mechanism: declare a function or property with a receiver type — the function call syntax receiver.extensionFunction() resolves to the extension. The extension is statically dispatched — resolved at compile time based on the static type of the receiver, not the runtime type. Extensions are the foundation of Kotlin’s substantial standard library — most of kotlin.collections.* is implemented as extensions on Iterable, Collection, List, Map, etc. The combination — extension functions and properties, the static-dispatch mechanism, the substantial DSL applications, the package-scoped admission — is the substance of Kotlin’s extension surface.

Extension functions

The form: fun ReceiverType.functionName(params): ReturnType { body }:

fun String.shout(): String = this.uppercase() + "!"

"hello".shout()                                    // "HELLO!"

Inside the extension, this refers to the receiver — the value the function is called on.

For a parameter-style implementation:

fun String.repeat(n: Int): String {
    val sb = StringBuilder()
    repeat(n) { sb.append(this) }
    return sb.toString()
}

"abc".repeat(3)                                    // "abcabcabc"

Extension functions for transformations

fun Int.isEven(): Boolean = this % 2 == 0
fun Int.isOdd(): Boolean = !isEven()
fun Int.factorial(): Long = if (this <= 1) 1 else this * (this - 1).factorial()

5.isEven()                                         // false
5.isOdd()                                          // true
5.factorial()                                      // 120

Extensions on generic types

fun <T> List<T>.secondOrNull(): T? = if (size < 2) null else this[1]

fun <T : Comparable<T>> List<T>.minMax(): Pair<T, T>? {
    if (isEmpty()) return null
    var min = first()
    var max = first()
    for (item in this) {
        if (item < min) min = item
        if (item > max) max = item
    }
    return min to max
}

listOf(3, 1, 4, 1, 5).minMax()                     // (1, 5)

Nullable receiver

Extensions admit nullable receivers — admit calling on null:

fun String?.isNullOrBlank(): Boolean = this == null || this.isBlank()

val s: String? = null
s.isNullOrBlank()                                  // true; admitted on null receiver

The mechanism admits substantial null-safe extensions — the isNullOrBlank and friends in the standard library use this pattern.

For non-nullable extensions on nullable types via safe call:

fun String.shout() = this.uppercase() + "!"

val s: String? = "hello"
s?.shout()                                         // "HELLO!" or null

Extension properties

Properties without backing fields:

val String.firstChar: Char?
    get() = if (isEmpty()) null else this[0]

"hello".firstChar                                  // 'h'
"".firstChar                                       // null

val Int.isPositive: Boolean
    get() = this > 0

5.isPositive                                       // true
(-3).isPositive                                    // false

Extension properties cannot have backing fields — must use a getter (and optionally a setter). For a var extension:

val map = mutableMapOf<String, Any>()

var Any.key: String
    get() = map.entries.firstOrNull { it.value == this }?.key ?: ""
    set(value) {
        map[value] = this
    }

42.key = "answer"                                   // sets map["answer"] = 42

The pattern is rare in idiomatic Kotlin — admit substantial conceptual complexity.

Static dispatch

Extensions are statically dispatched — resolved by the compile-time type of the receiver, not the runtime type:

open class Animal
class Dog : Animal()

fun Animal.describe() = "I'm an animal"
fun Dog.describe() = "I'm a dog"

val a: Animal = Dog()
a.describe()                                       // "I'm an animal" — static dispatch on Animal type

The mechanism distinguishes extensions from polymorphic methods. The conventional defence is avoiding extension functions when polymorphism is needed; use member functions instead.

Extensions vs members

When both an extension and a member exist with the same signature, the member wins:

class Foo {
    fun bar() = "member"
}

fun Foo.bar() = "extension"                        // shadowed by member

Foo().bar()                                        // "member"

The mechanism admits substantial backward compatibility — adding a member doesn’t break consumers using extensions.

Companion-object extensions

For static-style member extensions, extend the companion object:

class User(val name: String) {
    companion object
}

fun User.Companion.create(name: String): User = User(name.trim())

User.create("  Alice  ")                           // User("Alice")

The pattern admits adding factory methods or constants to existing types.

Local extensions

Extensions may be declared inside a class — visible only within that class:

class Service {
    private fun String.toEntity(): Entity {        // local extension
        return Entity(this.trim())
    }

    fun process(input: String): Entity {
        return input.toEntity()                    // local extension visible
    }
}

// Outside Service, the extension is not admitted.

The mechanism admits substantial encapsulation.

Member extensions

A class may declare extensions as members — visible within the class:

class Container {
    private val items = mutableListOf<String>()

    fun String.add() {                             // member extension on String
        items.add(this)
    }

    fun process() {
        "first".add()                              // call member extension
        "second".add()
    }
}

// Outside Container, "x".add() is not admitted.

The mechanism admits class-scoped extension functions.

Common patterns

Validation helpers

fun String.isValidEmail(): Boolean = matches(Regex("[^@]+@[^@]+\\.[^@]+"))
fun String.isValidPhone(): Boolean = matches(Regex("\\+?\\d{10,}"))
fun String.isValidUUID(): Boolean = matches(Regex("[a-f0-9-]{36}"))

"alice@example.com".isValidEmail()                 // true

Conversion helpers

fun String.toIntSafe(default: Int = 0): Int = toIntOrNull() ?: default
fun Long.toDuration(): Duration = milliseconds(this)
fun Date.toLocalDate(): LocalDate = LocalDate.ofInstant(toInstant(), ZoneId.systemDefault())

DSL building blocks

class HtmlBuilder {
    private val parts = mutableListOf<String>()

    fun h1(content: String) { parts.add("<h1>$content</h1>") }
    fun p(content: String) { parts.add("<p>$content</p>") }

    override fun toString() = parts.joinToString("\n")
}

fun html(block: HtmlBuilder.() -> Unit): String {
    return HtmlBuilder().apply(block).toString()
}

val page = html {
    h1("Welcome")
    p("Hello, world")
}

The lambda-with-receiver admits substantial DSL syntax.

Null-safe extensions

fun String?.orDefault(default: String): String = this ?: default

val name: String? = null
name.orDefault("anonymous")                        // "anonymous"

fun Collection<*>?.isNullOrEmpty(): Boolean = this == null || isEmpty()

Standard library examples

The Kotlin stdlib uses extensions substantially:

// kotlin.collections.Iterable extensions:
fun <T> Iterable<T>.map(transform: (T) -> R): List<R>
fun <T> Iterable<T>.filter(predicate: (T) -> Boolean): List<T>
fun <T> Iterable<T>.sortedBy(selector: (T) -> Comparable<*>?): List<T>

// kotlin.text.String extensions:
fun String.toIntOrNull(): Int?
fun String.isBlank(): Boolean
fun String.padStart(length: Int, padChar: Char = ' '): String

Builder DSL

class FormBuilder {
    private val fields = mutableListOf<Field>()

    fun text(name: String, label: String) {
        fields.add(Field.Text(name, label))
    }

    fun number(name: String, label: String) {
        fields.add(Field.Number(name, label))
    }

    fun submit(label: String = "Submit") {
        fields.add(Field.Submit(label))
    }

    fun build(): Form = Form(fields.toList())
}

fun form(builder: FormBuilder.() -> Unit): Form {
    return FormBuilder().apply(builder).build()
}

val loginForm = form {
    text("email", "Email")
    text("password", "Password")
    submit("Login")
}

Domain-specific operators

operator fun Money.plus(other: Money): Money {
    require(currency == other.currency) { "currency mismatch" }
    return Money(amount + other.amount, currency)
}

operator fun Money.times(factor: Int): Money {
    return Money(amount * factor.toBigDecimal(), currency)
}

val total = Money(100.toBigDecimal(), "USD") + Money(50.toBigDecimal(), "USD")
val tripled = Money(50.toBigDecimal(), "USD") * 3

Type-checking helpers

inline fun <reified T> Any?.isInstance(): Boolean = this is T

val anyList: List<Any> = listOf(1, "two", 3.0)
anyList.first().isInstance<Int>()                  // true
anyList.first().isInstance<String>()               // false

Sequence operations

fun <T> Sequence<T>.takeWhileTotal(limit: Int, getValue: (T) -> Int): List<T> {
    var total = 0
    return takeWhile {
        total += getValue(it)
        total <= limit
    }.toList()
}

(1..100).asSequence()
    .takeWhileTotal(50) { it }
    // [1, 2, 3, ..., until total exceeds 50]

Functional combinators

inline fun <T, R> T?.then(transform: (T) -> R?): R? {
    return this?.let(transform)
}

val emailHash = email.then { md5(it) }

Receiver-style extensions for DSLs

fun StringBuilder.appendLineWithPrefix(prefix: String, content: String) {
    appendLine("$prefix $content")
}

val report = buildString {
    appendLineWithPrefix("INFO:", "Started")
    appendLineWithPrefix("ERROR:", "Connection failed")
}

Companion-object extension for types

class Money(val amount: BigDecimal, val currency: String) {
    companion object
}

fun Money.Companion.fromString(s: String): Money? {
    val parts = s.split(" ")
    if (parts.size != 2) return null
    val amount = parts[0].toBigDecimalOrNull() ?: return null
    return Money(amount, parts[1])
}

Money.fromString("100.50 USD")                     // Money(100.50, USD)

Generic extension function

fun <T> List<T>.shuffled(): List<T> {
    val mutable = toMutableList()
    java.util.Collections.shuffle(mutable)
    return mutable
}

inline fun <reified T> Any?.cast(): T? = this as? T

Extending platform types

fun Date.daysUntil(other: Date): Long {
    return TimeUnit.MILLISECONDS.toDays(other.time - this.time)
}

fun String.toBase64(): String =
    java.util.Base64.getEncoder().encodeToString(toByteArray())

fun String.fromBase64(): ByteArray =
    java.util.Base64.getDecoder().decode(this)

Extension for fluent chaining

fun <T> T.assertNotNull(message: String = "value was null"): T {
    if (this == null) throw IllegalStateException(message)
    return this
}

val name: String = nullable.assertNotNull("name required")

Scoping extensions to a function

fun process() {
    fun Int.timesProduct(other: Int) = this * other  // local extension

    val result = 5.timesProduct(3)                   // 15
}

The scoped extension admits substantial encapsulation.

A note on extension dispatch

fun Animal.describe() = "An animal"
fun Dog.describe() = "A dog"

class Animal
class Dog : Animal()

val animal: Animal = Dog()
animal.describe()                                   // "An animal" (static dispatch)

// Members are dynamic:
open class A {
    open fun describe() = "A"
}

class B : A() {
    override fun describe() = "B"
}

val a: A = B()
a.describe()                                        // "B" (dynamic dispatch)

The mechanism distinguishes extensions from polymorphic methods — extensions admit substantial reusability but lack runtime polymorphism.

A note on the conventional discipline

The contemporary Kotlin extensions advice:

  • Use extension functions for adding behaviour to existing types.
  • Use extension properties for derived values without state.
  • Use null-safe extensions (nullable receiver) for substantial null-safety.
  • Use lambda-with-receiver for DSL-building.
  • Extend the companion object for factory-style additions.
  • Use top-level extensions in Extensions.kt files for utility libraries.
  • Avoid extending types you don’t own with substantial behaviour — admit substantial confusion.
  • Prefer member functions when polymorphism is required.
  • Use inline reified extensions for substantial generic patterns.
  • Use member extensions for class-scoped extensions.

The combination — extension functions and properties, the static-dispatch mechanism, the lambda-with-receiver DSL pattern, the substantial standard library extensions, the nullable-receiver admission, the companion-object extension form — is the substance of Kotlin’s extension surface. The discipline produces concise, expressive, library-friendly code with substantial reuse without modifying existing types.