Skip to content

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.

TypeScriptGoKotlin
Array<T>[]T (slice)List<T> (immutable) / MutableList<T>
Set<T>map[T]struct{}Set<T> / MutableSet<T>
Map<K,V>map[K]VMap<K,V> / MutableMap<K,V>
Mutable by defaultMutable by defaultImmutable by default

The biggest difference: Kotlin collections are immutable by default. You choose mutability explicitly by using MutableList, MutableSet, or MutableMap.

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, remove
const numbers: number[] = [1, 2, 3, 4, 5];
numbers.push(6); // arrays are always mutable in TS
const readonly: readonly number[] = [1, 2, 3]; // type-level only -- can be cast away
const ids = new Set<number>([1, 2, 3]);
ids.add(4);
ids.has(2); // true
ids.delete(1);
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 object
const config: Record<string, string> = {
host: "localhost",
port: "8080",
};

Key Differences:

  • to is an infix function that creates a Pair: 1 to "Alice" creates Pair(1, "Alice").
  • Map access with [] returns nullable: map[key] returns V?, never throws.
  • Go’s comma-ok pattern (val, ok := map[key]) becomes nullable in Kotlin (map[key]).
// Empty collections
val emptyList: List<String> = emptyList()
val emptySet: Set<Int> = emptySet()
val emptyMap: Map<String, Int> = emptyMap()
// From values
val list = listOf(1, 2, 3)
val set = setOf("a", "b", "c")
val map = mapOf("key1" to 1, "key2" to 2)
// Mutable from values
val 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 arrays
val array = intArrayOf(1, 2, 3)
val listFromArray: List<Int> = array.toList()
// From other collections
val listFromSet: List<String> = setOf("b", "a", "c").toList()
val setFromList: Set<Int> = listOf(1, 2, 2, 3, 3).toSet() // removes duplicates
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]
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]
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"]
const result = users
.filter(u => u.active)
.map(u => u.name)
.sort()
.join(", ");

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”
const sum = [1, 2, 3, 4, 5].reduce((acc, n) => acc + n, 0); // 15
const product = [1, 2, 3, 4, 5].reduce((acc, n) => acc * n, 1); // 120
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 lodash
const byDept = users.reduce((acc, u) => {
(acc[u.dept] ??= []).push(u);
return acc;
}, {} as Record<string, typeof users>);

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 element
val byName: Map<String, User> = users.associateBy { it.name }
// {Alice=User(Alice, Eng), Bob=User(Bob, Mkt)}
// associateWith -- key is the element, value selector
val nameLengths: Map<String, Int> = listOf("alice", "bob", "charlie")
.associateWith { it.length }
// {alice=5, bob=3, charlie=7}
// associate -- full control over key and value
val 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 numbers = listOf(1, 5, 3, 8, 2, 9, 4, 7, 6)
// Finding elements
numbers.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 conditions
numbers.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) // true
5 in numbers // true (same as contains)
// Take and drop
numbers.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]
// Distinct
listOf(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]]
const names = ["Charlie", "Alice", "Bob"];
names.sort(); // mutates in place!
const sorted = [...names].sort(); // copy then sort
const byLength = [...names].sort((a, b) => a.length - b.length);

When you chain .map().filter().take(), each step creates an intermediate list:

// EAGER: creates 3 intermediate lists
val 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!
// LAZY: processes elements one at a time, stops when take(10) is satisfied
val 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!
TypeScriptGoKotlin
No built-in lazy (use generators or RxJS)No built-in lazy (use channels)Sequence<T>
function* generatorsfunc yield() (Go 1.23+)sequence { yield(x) }
Lodash .chain().asSequence()
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 indexOnly iterate once
Need to reuse the resultCan recompute if needed
// From existing collection
val seq = listOf(1, 2, 3).asSequence()
// From values
val 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 generateSequence
val 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]

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 list
seq.toSet() // collects into a set
seq.first() // finds first element
seq.count() // counts elements
seq.sum() // sums elements
seq.forEach { } // iterates
seq.any { it > 5 } // checks condition

Scope 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.

FunctionObject refReturn valueUse case
letitLambda resultNull checks, transform, scoping
runthisLambda resultObject configuration + compute
withthisLambda resultGrouping calls on an object
applythisThe objectObject configuration (builder)
alsoitThe objectSide effects (logging, validation)
// No direct equivalent. Closest is optional chaining + IIFE
const name: string | null = findUserName();
if (name !== null) {
console.log(`Found user: ${name}`);
sendEmail(name);
}
// Compute a result from an object's context
val greeting = "World".run {
"Hello, ${this.uppercase()}!" // 'this' is the String "World"
}
// "Hello, WORLD!"
// Configure and get result
val response = HttpClient().run {
setBaseUrl("https://api.example.com")
addHeader("Authorization", "Bearer token")
get("/users") // return value
}
// Group operations on an existing object
val 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 objects
val 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 calls
const user = Object.assign(new User(), {
name: "Alice",
age: 30,
email: "alice@example.com",
});

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 chain
fun getUser(id: Long): User {
return userRepository.findById(id)
.also { println("Found user: $it") }
?: throw NotFoundException("User $id not found")
}
// Debug intermediate values
val result = numbers
.map { it * 2 }
.also { println("After map: $it") }
.filter { it > 3 }
.also { println("After filter: $it") }
Need to transform/map a value? → let
Need null-safe operations? → let (with ?.)
Need to configure + return something? → run
Grouping calls on a non-null object? → with
Building/configuring an object? → apply
Adding side effects to a chain? → also
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"
}
// Array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
// Object destructuring
const { name, age, email } = user;
const { name: userName, ...otherProps } = user;

Key Differences:

  • Kotlin destructuring is positional, not name-based (unlike TypeScript object destructuring).
  • Only data class, Pair, Triple, and classes with componentN() operators can be destructured.
  • Kotlin has no spread/rest operator in destructuring (...rest doesn’t exist).
// Any class can support destructuring by defining componentN() operators
class 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=0

Kotlin’s inline keyword eliminates the overhead of lambda-based functions.

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 runtime
fun 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() }
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() }
}
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
)
}
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"
}
// Common pattern: convert a list to a lookup map
data 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 ID
val productById: Map<String, Product> = products.associateBy { it.id }
val widget = productById["P1"] // Product(P1, Widget, 9.99)
// Name to price lookup
val priceByName: Map<String, Double> = products.associate { it.name to it.price }
val widgetPrice = priceByName["Widget"] // 9.99
// Group by price range
val byPriceRange: Map<String, List<Product>> = products.groupBy {
when {
it.price < 5.0 -> "budget"
it.price < 20.0 -> "mid-range"
else -> "premium"
}
}

Put these operations to work — build a real data processing pipeline end to end.