Collections, Sequences & FP
Kotlin’s standard library gives you TypeScript-level convenience for data transformations with Go-level performance control. This module covers collections, functional operations, lazy sequences, scope functions, and destructuring.
Collections Overview
Section titled “Collections Overview”Mental model
Section titled “Mental model”| TypeScript | Go | Kotlin |
|---|---|---|
Array<T> | []T (slice) | List<T> (immutable) / MutableList<T> |
Set<T> | map[T]struct{} | Set<T> / MutableSet<T> |
Map<K,V> | map[K]V | Map<K,V> / MutableMap<K,V> |
| Mutable by default | Mutable by default | Immutable by default |
The biggest difference: Kotlin collections are immutable by default. You
choose mutability explicitly by using MutableList, MutableSet, or
MutableMap.
The collection hierarchy
Section titled “The collection hierarchy”Iterable<T>└── Collection<T> // read-only: size, contains, iterator ├── List<T> // ordered, indexed access, duplicates allowed ├── Set<T> // unique elements, no guaranteed order (HashSet) └── MutableCollection<T> ├── MutableList<T> // add, remove, set by index └── MutableSet<T> // add, remove
Map<K,V> // read-only: keys, values, entries└── MutableMap<K,V> // put, removeList, Set, Map — Immutable vs Mutable
Section titled “List, Set, Map — Immutable vs Mutable”const numbers: number[] = [1, 2, 3, 4, 5];numbers.push(6); // arrays are always mutable in TSconst readonly: readonly number[] = [1, 2, 3]; // type-level only -- can be cast awaynumbers := []int{1, 2, 3, 4, 5}numbers = append(numbers, 6) // slices are always mutable// No built-in immutable slice// Immutable (read-only) list -- no add, remove, or set operationsval numbers: List<Int> = listOf(1, 2, 3, 4, 5)// numbers.add(6) // COMPILE ERROR: List has no add method
// Mutable list -- full mutabilityval mutableNumbers: MutableList<Int> = mutableListOf(1, 2, 3, 4, 5)mutableNumbers.add(6) // OKmutableNumbers.removeAt(0) // OKmutableNumbers[0] = 99 // OK
// Convert between themval readOnly: List<Int> = mutableNumbers.toList() // snapshot copyval mutable: MutableList<Int> = numbers.toMutableList() // mutable copyconst ids = new Set<number>([1, 2, 3]);ids.add(4);ids.has(2); // trueids.delete(1);// Go has no built-in Set. Use map[T]struct{} or map[T]bool.ids := map[int]struct{}{1: {}, 2: {}, 3: {}}ids[4] = struct{}{} // add_, exists := ids[2] // check: exists == truedelete(ids, 1) // remove// Immutable setval ids: Set<Int> = setOf(1, 2, 3)println(2 in ids) // true (same as ids.contains(2))
// Mutable setval mutableIds: MutableSet<Int> = mutableSetOf(1, 2, 3)mutableIds.add(4) // true (added)mutableIds.add(2) // false (already exists)mutableIds.remove(1) // true (removed)
// Set operations (like mathematical sets)val a = setOf(1, 2, 3, 4)val b = setOf(3, 4, 5, 6)println(a union b) // [1, 2, 3, 4, 5, 6]println(a intersect b) // [3, 4]println(a subtract b) // [1, 2]const userMap = new Map<number, string>();userMap.set(1, "Alice");userMap.set(2, "Bob");console.log(userMap.get(1)); // "Alice"console.log(userMap.has(3)); // false
// Or plain objectconst config: Record<string, string> = { host: "localhost", port: "8080",};userMap := map[int]string{ 1: "Alice", 2: "Bob",}name, ok := userMap[1] // "Alice", true_, exists := userMap[3] // "", falseuserMap[3] = "Charlie" // adddelete(userMap, 1) // remove// Immutable mapval userMap: Map<Int, String> = mapOf( 1 to "Alice", 2 to "Bob")println(userMap[1]) // "Alice"println(userMap[3]) // null (no exception!)println(userMap.getOrDefault(3, "Unknown")) // "Unknown"println(1 in userMap) // true (checks keys)
// Mutable mapval mutableMap: MutableMap<Int, String> = mutableMapOf( 1 to "Alice", 2 to "Bob")mutableMap[3] = "Charlie" // add/updatemutableMap.remove(1) // removemutableMap.putIfAbsent(2, "Bobby") // no-op, key 2 exists
// Iteratefor ((key, value) in userMap) { println("$key -> $value")}Key Differences:
tois an infix function that creates aPair:1 to "Alice"createsPair(1, "Alice").- Map access with
[]returns nullable:map[key]returnsV?, never throws. - Go’s comma-ok pattern (
val, ok := map[key]) becomes nullable in Kotlin (map[key]).
Creating Collections
Section titled “Creating Collections”// Empty collectionsval emptyList: List<String> = emptyList()val emptySet: Set<Int> = emptySet()val emptyMap: Map<String, Int> = emptyMap()
// From valuesval list = listOf(1, 2, 3)val set = setOf("a", "b", "c")val map = mapOf("key1" to 1, "key2" to 2)
// Mutable from valuesval mutableList = mutableListOf(1, 2, 3)val mutableSet = mutableSetOf("a", "b", "c")val mutableMap = mutableMapOf("key1" to 1, "key2" to 2)
// From a size and init function (like Array.from in TS)val squares = List(5) { i -> i * i } // [0, 1, 4, 9, 16]val indices = MutableList(10) { it } // [0, 1, 2, ..., 9]
// buildList / buildSet / buildMap (builder pattern)val config = buildMap { put("host", "localhost") put("port", "8080") if (System.getenv("DEBUG") != null) { put("debug", "true") }}
val items = buildList { add("always") addAll(listOf("a", "b", "c")) if (condition) add("conditional")}
// From arraysval array = intArrayOf(1, 2, 3)val listFromArray: List<Int> = array.toList()
// From other collectionsval listFromSet: List<String> = setOf("b", "a", "c").toList()val setFromList: Set<Int> = listOf(1, 2, 2, 3, 3).toSet() // removes duplicatesTransformations: map, filter, flatMap
Section titled “Transformations: map, filter, flatMap”const names = ["alice", "bob", "charlie"];const upper = names.map(n => n.toUpperCase());// ["ALICE", "BOB", "CHARLIE"]
const users = [{ name: "Alice", age: 30 }, { name: "Bob", age: 25 }];const ages = users.map(u => u.age);// [30, 25]names := []string{"alice", "bob", "charlie"}upper := make([]string, len(names))for i, n := range names { upper[i] = strings.ToUpper(n)}val names = listOf("alice", "bob", "charlie")val upper = names.map { it.uppercase() }// [ALICE, BOB, CHARLIE]
data class User(val name: String, val age: Int)val users = listOf(User("Alice", 30), User("Bob", 25))val ages = users.map { it.age }// [30, 25]
// mapIndexed (like TS map with index parameter)val indexed = names.mapIndexed { i, name -> "$i: $name" }// ["0: alice", "1: bob", "2: charlie"]
// mapNotNull (map + filter nulls)val numbers = listOf("1", "two", "3", "four")val parsed = numbers.mapNotNull { it.toIntOrNull() }// [1, 3] -- skips nullsfilter
Section titled “filter”const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];const evens = numbers.filter(n => n % 2 === 0);// [2, 4, 6, 8, 10]numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}var evens []intfor _, n := range numbers { if n%2 == 0 { evens = append(evens, n) }}val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)val evens = numbers.filter { it % 2 == 0 }// [2, 4, 6, 8, 10]
// filterNot (inverse filter)val odds = numbers.filterNot { it % 2 == 0 }// [1, 3, 5, 7, 9]
// filterIsInstance (filter by type -- very useful!)val mixed: List<Any> = listOf(1, "hello", 2.0, "world", 3)val strings: List<String> = mixed.filterIsInstance<String>()// ["hello", "world"]
// filterNotNullval nullable: List<String?> = listOf("hello", null, "world", null)val nonNull: List<String> = nullable.filterNotNull()// ["hello", "world"]
// partition (split into two lists based on predicate)val (passed, failed) = numbers.partition { it >= 5 }// passed = [5, 6, 7, 8, 9, 10], failed = [1, 2, 3, 4]flatMap
Section titled “flatMap”const nested = [[1, 2], [3, 4], [5, 6]];const flat = nested.flat();// [1, 2, 3, 4, 5, 6]
const users = [ { name: "Alice", roles: ["admin", "user"] }, { name: "Bob", roles: ["user"] },];const allRoles = users.flatMap(u => u.roles);// ["admin", "user", "user"]val nested = listOf(listOf(1, 2), listOf(3, 4), listOf(5, 6))val flat = nested.flatten()// [1, 2, 3, 4, 5, 6]
data class User(val name: String, val roles: List<String>)val users = listOf( User("Alice", listOf("admin", "user")), User("Bob", listOf("user")),)val allRoles = users.flatMap { it.roles }// ["admin", "user", "user"]
// flatMap is map + flattenval result = users.flatMap { user -> user.roles.map { role -> "${user.name}:$role" }}// ["Alice:admin", "Alice:user", "Bob:user"]Chaining operations
Section titled “Chaining operations”const result = users .filter(u => u.active) .map(u => u.name) .sort() .join(", ");val result = users .filter { it.active } .map { it.name } .sorted() .joinToString(", ")Almost identical syntax. The key difference is Kotlin uses { } lambdas instead
of => arrows and it for single parameters.
Aggregations: reduce, fold, groupBy, associate
Section titled “Aggregations: reduce, fold, groupBy, associate”reduce and fold
Section titled “reduce and fold”const sum = [1, 2, 3, 4, 5].reduce((acc, n) => acc + n, 0); // 15const product = [1, 2, 3, 4, 5].reduce((acc, n) => acc * n, 1); // 120numbers := []int{1, 2, 3, 4, 5}sum := 0for _, n := range numbers { sum += n}val numbers = listOf(1, 2, 3, 4, 5)
// reduce -- uses first element as initial accumulatorval sum = numbers.reduce { acc, n -> acc + n } // 15
// fold -- you provide the initial value (like TS reduce with initial)val sum2 = numbers.fold(0) { acc, n -> acc + n } // 15val product = numbers.fold(1) { acc, n -> acc * n } // 120
// Kotlin shorthand for common aggregationsnumbers.sum() // 15numbers.average() // 3.0numbers.count() // 5numbers.min() // 1numbers.max() // 5numbers.minOrNull() // 1 (null-safe version)numbers.maxOrNull() // 5 (null-safe version)
// sumOf for computed valuesdata class Item(val name: String, val price: Double, val quantity: Int)val items = listOf(Item("Widget", 9.99, 3), Item("Gadget", 24.99, 1))val total = items.sumOf { it.price * it.quantity } // 54.96groupBy
Section titled “groupBy”const users = [ { name: "Alice", dept: "Engineering" }, { name: "Bob", dept: "Marketing" }, { name: "Charlie", dept: "Engineering" }, { name: "Diana", dept: "Marketing" },];
// No built-in groupBy in TS -- you use reduce or lodashconst byDept = users.reduce((acc, u) => { (acc[u.dept] ??= []).push(u); return acc;}, {} as Record<string, typeof users>);byDept := make(map[string][]User)for _, u := range users { byDept[u.Dept] = append(byDept[u.Dept], u)}data class User(val name: String, val dept: String)val users = listOf( User("Alice", "Engineering"), User("Bob", "Marketing"), User("Charlie", "Engineering"), User("Diana", "Marketing"),)
// groupBy -- returns Map<K, List<V>>val byDept: Map<String, List<User>> = users.groupBy { it.dept }// {Engineering=[User(Alice, Engineering), User(Charlie, Engineering)],// Marketing=[User(Bob, Marketing), User(Diana, Marketing)]}
// groupBy with value transformval namesByDept: Map<String, List<String>> = users.groupBy( keySelector = { it.dept }, valueTransform = { it.name })// {Engineering=[Alice, Charlie], Marketing=[Bob, Diana]}
// Counting per groupval countByDept: Map<String, Int> = users.groupingBy { it.dept }.eachCount()// {Engineering=2, Marketing=2}associate
Section titled “associate”associate creates a Map from a List — the inverse of map.
val users = listOf(User("Alice", "Eng"), User("Bob", "Mkt"))
// associateBy -- key selector, value is the elementval byName: Map<String, User> = users.associateBy { it.name }// {Alice=User(Alice, Eng), Bob=User(Bob, Mkt)}
// associateWith -- key is the element, value selectorval nameLengths: Map<String, Int> = listOf("alice", "bob", "charlie") .associateWith { it.length }// {alice=5, bob=3, charlie=7}
// associate -- full control over key and valueval nameToUpper: Map<String, String> = listOf("alice", "bob") .associate { it to it.uppercase() }// {alice=ALICE, bob=BOB}// No built-in zip. Use lodash or manual loop.const keys = ["a", "b", "c"];const values = [1, 2, 3];const zipped = keys.map((k, i) => [k, values[i]]);val keys = listOf("a", "b", "c")val values = listOf(1, 2, 3)
// zip creates a list of pairsval zipped: List<Pair<String, Int>> = keys.zip(values)// [(a, 1), (b, 2), (c, 3)]
// zip with transformval mapped: List<String> = keys.zip(values) { k, v -> "$k=$v" }// ["a=1", "b=2", "c=3"]
// Convert zipped pairs to mapval map: Map<String, Int> = keys.zip(values).toMap()// {a=1, b=2, c=3}
// unzipval (unzippedKeys, unzippedValues) = zipped.unzip()Filtering and Searching
Section titled “Filtering and Searching”val numbers = listOf(1, 5, 3, 8, 2, 9, 4, 7, 6)
// Finding elementsnumbers.first() // 1 (throws if empty)numbers.firstOrNull() // 1 (null if empty)numbers.first { it > 5 } // 8 (first matching)numbers.firstOrNull { it > 100 } // null (no match)numbers.last { it < 5 } // 4 (last matching)numbers.single { it == 5 } // 5 (throws if not exactly one)numbers.singleOrNull { it > 8 } // 9 (null if not exactly one)
// Checking conditionsnumbers.any { it > 5 } // true (at least one matches)numbers.all { it > 0 } // true (all match)numbers.none { it < 0 } // true (none match)numbers.contains(5) // true5 in numbers // true (same as contains)
// Take and dropnumbers.take(3) // [1, 5, 3]numbers.takeLast(3) // [4, 7, 6]numbers.drop(3) // [8, 2, 9, 4, 7, 6]numbers.dropLast(3) // [1, 5, 3, 8, 2, 9]numbers.takeWhile { it < 8 } // [1, 5, 3]numbers.dropWhile { it < 8 } // [8, 2, 9, 4, 7, 6]
// DistinctlistOf(1, 2, 2, 3, 3, 3).distinct() // [1, 2, 3]listOf("alice", "ALICE", "Bob").distinctBy { // ["alice", "Bob"] it.lowercase()}
// Chunked and windowed(1..10).toList().chunked(3) // [[1,2,3], [4,5,6], [7,8,9], [10]](1..5).toList().windowed(3) // [[1,2,3], [2,3,4], [3,4,5]](1..5).toList().windowed(3, step = 2) // [[1,2,3], [3,4,5]]Sorting and Ordering
Section titled “Sorting and Ordering”const names = ["Charlie", "Alice", "Bob"];names.sort(); // mutates in place!const sorted = [...names].sort(); // copy then sortconst byLength = [...names].sort((a, b) => a.length - b.length);names := []string{"Charlie", "Alice", "Bob"}sort.Strings(names) // mutates in placesort.Slice(names, func(i, j int) bool { return len(names[i]) < len(names[j])})val names = listOf("Charlie", "Alice", "Bob")
// sorted() returns a NEW sorted list (immutable-friendly)val sorted = names.sorted() // [Alice, Bob, Charlie]val reversed = names.sortedDescending() // [Charlie, Bob, Alice]
// sortedBy (like TS sort with comparator, but cleaner)val byLength = names.sortedBy { it.length } // [Bob, Alice, Charlie]val byLengthDesc = names.sortedByDescending { it.length } // [Charlie, Alice, Bob]
// sortedWith (full custom comparator)val custom = names.sortedWith(compareBy<String> { it.length }.thenBy { it })// [Bob, Alice, Charlie] -- by length, then alphabetically
// Mutable sorting (mutates in place)val mutableNames = mutableListOf("Charlie", "Alice", "Bob")mutableNames.sort() // mutates in place, returns Unit
// Building comparatorsval comparator = compareBy<User> { it.dept } .thenByDescending { it.name } .thenBy { it.age }
val sortedUsers = users.sortedWith(comparator)
// Reverse a listnames.reversed() // [Bob, Alice, Charlie] (new list)names.asReversed() // reversed view (no copy, reflects changes)Sequences (Lazy Evaluation)
Section titled “Sequences (Lazy Evaluation)”The problem with eager collections
Section titled “The problem with eager collections”When you chain .map().filter().take(), each step creates an intermediate list:
// EAGER: creates 3 intermediate listsval result = (1..1_000_000) .toList() .map { it * 2 } // creates list of 1M elements .filter { it % 3 == 0 } // creates another list .take(10) // we only needed 10!Sequences: lazy pipeline
Section titled “Sequences: lazy pipeline”// LAZY: processes elements one at a time, stops when take(10) is satisfiedval result = (1..1_000_000) .asSequence() // convert to lazy sequence .map { it * 2 } // lazy: not executed yet .filter { it % 3 == 0 } // lazy: not executed yet .take(10) // lazy: not executed yet .toList() // TERMINAL operation: now it executes!Comparison: lazy evaluation
Section titled “Comparison: lazy evaluation”| TypeScript | Go | Kotlin |
|---|---|---|
| No built-in lazy (use generators or RxJS) | No built-in lazy (use channels) | Sequence<T> |
function* generators | func yield() (Go 1.23+) | sequence { yield(x) } |
Lodash .chain() | — | .asSequence() |
When to use sequences
Section titled “When to use sequences”Use List (eager) | Use Sequence (lazy) |
|---|---|
| Small collections (< 10K elements) | Large collections (> 10K elements) |
| Simple chains (1-2 operations) | Long chains (3+ operations) |
| Need random access by index | Only iterate once |
| Need to reuse the result | Can recompute if needed |
Creating sequences
Section titled “Creating sequences”// From existing collectionval seq = listOf(1, 2, 3).asSequence()
// From valuesval seq2 = sequenceOf(1, 2, 3)
// Generated sequence (infinite!)val naturals = generateSequence(1) { it + 1 } // 1, 2, 3, 4, ...val firstTen = naturals.take(10).toList() // [1, 2, 3, ..., 10]
// Fibonacci with generateSequenceval fibonacci = generateSequence(0 to 1) { (a, b) -> b to (a + b) } .map { it.first }val firstFibs = fibonacci.take(10).toList() // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
// sequence builder (like a generator)val primes = sequence { yield(2) var n = 3 while (true) { if ((2 until n).none { n % it == 0 }) { yield(n) } n += 2 }}println(primes.take(10).toList()) // [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]Terminal operations
Section titled “Terminal operations”Sequences are lazy. Nothing happens until a terminal operation:
val seq = listOf(1, 2, 3, 4, 5).asSequence() .map { println("map $it"); it * 2 } .filter { println("filter $it"); it > 4 }// Nothing printed yet!
// Terminal operations that trigger evaluation:seq.toList() // collects into a listseq.toSet() // collects into a setseq.first() // finds first elementseq.count() // counts elementsseq.sum() // sums elementsseq.forEach { } // iteratesseq.any { it > 5 } // checks conditionScope Functions: let, run, with, apply, also
Section titled “Scope Functions: let, run, with, apply, also”Scope functions are a uniquely Kotlin feature. They execute a block of code in
the context of an object. There are five: let, run, with, apply, also.
Quick reference
Section titled “Quick reference”| Function | Object ref | Return value | Use case |
|---|---|---|---|
let | it | Lambda result | Null checks, transform, scoping |
run | this | Lambda result | Object configuration + compute |
with | this | Lambda result | Grouping calls on an object |
apply | this | The object | Object configuration (builder) |
also | it | The object | Side effects (logging, validation) |
let — Transform or null-check
Section titled “let — Transform or null-check”// No direct equivalent. Closest is optional chaining + IIFEconst name: string | null = findUserName();if (name !== null) { console.log(`Found user: ${name}`); sendEmail(name);}// Null safety pattern (most common use)val name: String? = findUserName()name?.let { println("Found user: $it") sendEmail(it)}
// Transform and scopeval result = "Hello, World" .let { it.uppercase() } .let { it.take(5) }// "HELLO"
// Scoping (limit variable visibility)val dbResult = connection.let { conn -> val stmt = conn.prepareStatement("SELECT * FROM users") stmt.executeQuery()}run — Configure + compute result
Section titled “run — Configure + compute result”// Compute a result from an object's contextval greeting = "World".run { "Hello, ${this.uppercase()}!" // 'this' is the String "World"}// "Hello, WORLD!"
// Configure and get resultval response = HttpClient().run { setBaseUrl("https://api.example.com") addHeader("Authorization", "Bearer token") get("/users") // return value}with — Group calls on an object
Section titled “with — Group calls on an object”// Group operations on an existing objectval user = User("Alice", 30, "alice@example.com")val description = with(user) { // 'this' is user -- can access properties directly "Name: $name, Age: $age, Email: $email"}
// Useful for StringBuilder, Paint objects, config objectsval html = with(StringBuilder()) { appendLine("<html>") appendLine("<body>") appendLine("<h1>Hello</h1>") appendLine("</body>") appendLine("</html>") toString()}apply — Configure an object (builder pattern)
Section titled “apply — Configure an object (builder pattern)”// Closest is Object.assign or immediate method callsconst user = Object.assign(new User(), { name: "Alice", age: 30, email: "alice@example.com",});// apply returns the object itself -- perfect for configurationval user = User().apply { name = "Alice" age = 30 email = "alice@example.com"}
// Common: configure objects that need multiple settersval connection = DriverManager.getConnection(url).apply { autoCommit = false transactionIsolation = Connection.TRANSACTION_SERIALIZABLE}also — Side effects without modifying the chain
Section titled “also — Side effects without modifying the chain”// also returns the object -- use for side effects (logging, validation)val numbers = mutableListOf(1, 2, 3) .also { println("Initial: $it") } .also { require(it.isNotEmpty()) { "List must not be empty" } }
// Logging in a chainfun getUser(id: Long): User { return userRepository.findById(id) .also { println("Found user: $it") } ?: throw NotFoundException("User $id not found")}
// Debug intermediate valuesval result = numbers .map { it * 2 } .also { println("After map: $it") } .filter { it > 3 } .also { println("After filter: $it") }Decision guide
Section titled “Decision guide”Need to transform/map a value? → letNeed null-safe operations? → let (with ?.)Need to configure + return something? → runGrouping calls on a non-null object? → withBuilding/configuring an object? → applyAdding side effects to a chain? → alsoCombined example
Section titled “Combined example”data class Request( var url: String = "", var method: String = "GET", var headers: MutableMap<String, String> = mutableMapOf(), var body: String? = null)
fun buildRequest(): Request { return Request().apply { url = "https://api.example.com/users" method = "POST" headers["Content-Type"] = "application/json" body = """{"name": "Alice"}""" }.also { println("Built request: ${it.method} ${it.url}") }}
fun processResponse(response: Response?): String { return response ?.takeIf { it.statusCode == 200 } ?.let { it.body } ?.let { parseJson(it) } ?.run { extractName() } ?: "Unknown"}Destructuring Declarations
Section titled “Destructuring Declarations”Basic destructuring
Section titled “Basic destructuring”// Array destructuringconst [first, second, ...rest] = [1, 2, 3, 4, 5];
// Object destructuringconst { name, age, email } = user;const { name: userName, ...otherProps } = user;// Only for multiple return valuesname, err := getUser(id)
// No general destructuring// Pair and Triple destructuringval (name, age) = Pair("Alice", 30)val (x, y, z) = Triple(1.0, 2.0, 3.0)
// Data class destructuring (by position, not by name!)data class User(val name: String, val age: Int, val email: String)val user = User("Alice", 30, "alice@example.com")val (name, age, email) = user
// Skip values with _val (_, userAge, _) = user
// In for loopsval map = mapOf("a" to 1, "b" to 2)for ((key, value) in map) { println("$key -> $value")}
// In lambdasval users = listOf(User("Alice", 30, "a@b.com"), User("Bob", 25, "b@b.com"))users.forEach { (name, age, _) -> println("$name is $age")}
// With withIndexval names = listOf("Alice", "Bob", "Charlie")for ((index, name) in names.withIndex()) { println("$index: $name")}Key Differences:
- Kotlin destructuring is positional, not name-based (unlike TypeScript object destructuring).
- Only
data class,Pair,Triple, and classes withcomponentN()operators can be destructured. - Kotlin has no spread/rest operator in destructuring (
...restdoesn’t exist).
Custom destructuring
Section titled “Custom destructuring”// Any class can support destructuring by defining componentN() operatorsclass Color(val hex: String) { operator fun component1(): Int = hex.substring(1, 3).toInt(16) // red operator fun component2(): Int = hex.substring(3, 5).toInt(16) // green operator fun component3(): Int = hex.substring(5, 7).toInt(16) // blue}
val (red, green, blue) = Color("#FF8800")println("R=$red, G=$green, B=$blue") // R=255, G=136, B=0Inline Functions
Section titled “Inline Functions”Kotlin’s inline keyword eliminates the overhead of lambda-based functions.
Why inline matters
Section titled “Why inline matters”When you pass a lambda in Kotlin, it creates an anonymous class instance at
runtime. For hot-path code, this overhead adds up. inline tells the compiler
to copy the function body and lambda body directly into the call site.
// Without inline: creates a Function object at runtimefun measure(block: () -> Unit) { val start = System.nanoTime() block() val elapsed = System.nanoTime() - start println("Took ${elapsed / 1_000_000}ms")}
// With inline: function body + lambda copied to call site (zero overhead)inline fun measureInline(block: () -> Unit) { val start = System.nanoTime() block() val elapsed = System.nanoTime() - start println("Took ${elapsed / 1_000_000}ms")}
// Both used the same way:measure { heavyComputation() }measureInline { heavyComputation() }Practical Patterns
Section titled “Practical Patterns”Pattern: data pipeline
Section titled “Pattern: data pipeline”data class LogEntry( val timestamp: String, val level: String, val service: String, val message: String)
fun processLogs(entries: List<LogEntry>): Map<String, List<String>> { return entries .filter { it.level == "ERROR" } .groupBy { it.service } .mapValues { (_, logs) -> logs.map { "${it.timestamp}: ${it.message}" } .sorted() } .filterValues { it.isNotEmpty() }}Pattern: building complex objects
Section titled “Pattern: building complex objects”data class Report( val title: String, val sections: List<Section>, val metadata: Map<String, String>)
data class Section(val heading: String, val content: String)
fun generateReport(data: AnalyticsData): Report { val sections = buildList { add(Section("Summary", data.summarize()))
data.anomalies.takeIf { it.isNotEmpty() }?.let { anomalies -> add(Section("Anomalies", anomalies.joinToString("\n"))) }
if (data.hasTimeSeries) { add(Section("Trends", data.analyzeTrends())) } }
val metadata = buildMap { put("generated", java.time.Instant.now().toString()) put("dataPoints", data.count.toString()) data.source?.let { put("source", it) } }
return Report( title = "Analytics Report - ${data.period}", sections = sections, metadata = metadata )}Pattern: null-safe processing chain
Section titled “Pattern: null-safe processing chain”data class Order( val id: String, val customer: Customer?, val items: List<OrderItem>)
data class Customer(val name: String, val email: String?, val tier: String?)data class OrderItem(val product: String, val price: Double, val quantity: Int)
fun getOrderSummary(orderId: String): String { return findOrder(orderId) ?.let { order -> val customerName = order.customer?.name ?: "Guest" val total = order.items.sumOf { it.price * it.quantity } val discount = order.customer?.tier?.let { tier -> when (tier) { "gold" -> 0.10 "silver" -> 0.05 else -> 0.0 } } ?: 0.0 val finalTotal = total * (1 - discount)
""" Order: ${order.id} Customer: $customerName Items: ${order.items.size} Total: $${"%.2f".format(finalTotal)} """.trimIndent() } ?: "Order $orderId not found"}Pattern: collection to lookup map
Section titled “Pattern: collection to lookup map”// Common pattern: convert a list to a lookup mapdata class Product(val id: String, val name: String, val price: Double)
val products = listOf( Product("P1", "Widget", 9.99), Product("P2", "Gadget", 24.99), Product("P3", "Doohickey", 4.99))
// Lookup by IDval productById: Map<String, Product> = products.associateBy { it.id }val widget = productById["P1"] // Product(P1, Widget, 9.99)
// Name to price lookupval priceByName: Map<String, Double> = products.associate { it.name to it.price }val widgetPrice = priceByName["Widget"] // 9.99
// Group by price rangeval byPriceRange: Map<String, List<Product>> = products.groupBy { when { it.price < 5.0 -> "budget" it.price < 20.0 -> "mid-range" else -> "premium" }}Practice
Section titled “Practice”Put these operations to work — build a real data processing pipeline end to end.