Chris Collins

Lab · Playground

Kotlin playground

Edit Kotlin here and JetBrains' Kotlin Playground compiles and runs it — data classes, null safety, sealed types with when, extension functions, sequences, and coroutines.

Code blocks on this page are editable — change them and press Run.

Kotlin is JetBrains' answer to "Java, but pleasant": a statically typed language for the JVM, announced in 2011, that keeps full Java interop while removing the boilerplate — data classes instead of hand-written equals/hashCode, type inference, and a type system that tracks which values can be null. Google made it the preferred language for Android in 2019, and it now compiles to JavaScript and native code too.

The Kotlin compiler is far too big to run in a browser, so these blocks send your code to JetBrains' Kotlin Playground service (api.kotlinlang.org), which compiles it with the latest stable Kotlin, runs it on the JVM, and sends back the output. That is the only thing that leaves your browser — the same arrangement as the Rust and Go playgrounds. Each block is a complete program with a main function.

Data classes: equality, copies, and destructuring for free
data class Point(val x: Int, val y: Int) {
    operator fun plus(o: Point) = Point(x + o.x, y + o.y)
    fun manhattan() = Math.abs(x) + Math.abs(y)
}

data class Robot(val name: String, val at: Point = Point(0, 0), val battery: Int = 100)

fun main() {
    val r1 = Robot("R2")
    val r2 = r1.copy(at = r1.at + Point(3, -4), battery = 92)   // a changed copy; r1 is untouched

    println(r1)
    println(r2)
    println("equal by value: ${Robot("R2") == r1}")

    val (name, at, battery) = r2                                 // destructuring
    println("$name is ${at.manhattan()} blocks from home with $battery% left")

    val fleet = setOf(r1, r2, Robot("R2"))                       // hashCode is generated too
    println("fleet size: ${fleet.size}")
}
Null safety: the type system knows what can be null
data class User(val name: String, val email: String?, val manager: User?)

fun domainOf(u: User): String =
    u.email?.substringAfter('@') ?: "(no email)"     // ?. stops at null; ?: supplies a default

fun main() {
    val boss = User("Alice", "alice@example.com", null)
    val dev = User("Bob", null, boss)

    println(domainOf(boss))
    println(domainOf(dev))
    println("Bob's manager's email: ${dev.manager?.email}")
    println("Alice's manager's email: ${boss.manager?.email}")

    val length: Int = dev.email?.length ?: 0
    println("length $length")

    // Smart casts: after the check, the compiler knows email is a String
    for (u in listOf(boss, dev)) {
        if (u.email != null) println("${u.name}: ${u.email.uppercase()}")
        else println("${u.name}: no email to shout")
    }
    // val oops: String = dev.email   // won't compile: String? is not String
}
Sealed types and when: exhaustive by construction
sealed interface Shape
data class Circle(val r: Double) : Shape
data class Rect(val w: Double, val h: Double) : Shape
data class Triangle(val a: Double, val b: Double, val c: Double) : Shape

fun area(s: Shape): Double = when (s) {               // no else: the compiler checks every case
    is Circle -> Math.PI * s.r * s.r                  // s is smart-cast to Circle here
    is Rect -> s.w * s.h
    is Triangle -> {
        val p = (s.a + s.b + s.c) / 2
        Math.sqrt(p * (p - s.a) * (p - s.b) * (p - s.c))
    }
}

fun main() {
    val shapes = listOf(Circle(1.0), Rect(2.0, 3.5), Triangle(3.0, 4.0, 5.0))
    for (s in shapes) println("%-40s %8.3f".format(s, area(s)))
    println("largest: ${shapes.maxBy(::area)}")
}
Extension functions and the collections library
// Add methods to types you don't own
fun String.isPalindrome(): Boolean {
    val letters = filter(Char::isLetter).lowercase()
    return letters == letters.reversed()
}
val Int.isEven get() = this % 2 == 0

data class Sale(val region: String, val item: String, val amount: Int)

fun main() {
    println(listOf("Racecar", "A man, a plan, a canal: Panama", "Kotlin").map { it to it.isPalindrome() })
    println((1..10).filter { it.isEven })

    val sales = listOf(
        Sale("north", "widgets", 1200), Sale("south", "widgets", 800),
        Sale("north", "gadgets", 450), Sale("east", "gizmos", 1500), Sale("south", "gadgets", 975),
    )
    println(sales.groupBy { it.region }.mapValues { (_, v) -> v.sumOf { it.amount } })
    println(sales.sortedByDescending { it.amount }.take(2).map { "${it.region}/${it.item}" })
    val (big, small) = sales.partition { it.amount >= 1000 }
    println("${big.size} big, ${small.size} small; items: ${sales.map { it.item }.distinct().sorted()}")
    println(sales.associate { it.item to it.region }.entries.joinToString())
}
Sequences: lazy pipelines and generators
fun collatz(start: Long) = generateSequence(start) { n ->
    if (n == 1L) null else if (n % 2 == 0L) n / 2 else 3 * n + 1
}

val primes = sequence {                    // a generator: yield suspends until the next value is wanted
    val found = mutableListOf<Int>()
    var n = 2
    while (true) {
        if (found.none { it * it <= n && n % it == 0 }) { found += n; yield(n) }
        n++
    }
}

fun main() {
    println(collatz(27).count())
    println(collatz(27).max())
    println(primes.take(15).toList())
    println(primes.dropWhile { it < 1000 }.first())
    // Laziness: only as much work as the answer needs
    var evaluated = 0
    val firstBig = (1..1_000_000).asSequence().map { evaluated++; it * it }.first { it > 50 }
    println("first square over 50 is $firstBig after $evaluated evaluations")
}
Coroutines: lightweight concurrency
import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis

suspend fun fetch(name: String, ms: Long): String {
    delay(ms)                              // suspends without blocking a thread
    return "$name (${ms} ms)"
}

fun main() = runBlocking {
    val time = measureTimeMillis {
        val a = async { fetch("prices", 300) }
        val b = async { fetch("stock", 200) }
        val c = async { fetch("reviews", 250) }
        println(listOf(a, b, c).awaitAll())
    }
    println("three 'requests' in about ${time / 100 * 100} ms, not 750")

    // 10,000 coroutines are cheap; 10,000 threads would not be
    val total = (1..10_000).map { i -> async { delay(10); i } }.awaitAll().sum()
    println("sum from 10,000 coroutines: $total")
}