PONY λ M2 Modula-2

Kotlin.CodeCompared.To/Swift

An interactive executable cheatsheet comparing Kotlin and Swift

Kotlin 2.3 Swift 6.3
Basics & Values
Hello, World
fun main() { println("Hello, World!") }
print("Hello, World!")
Swift scripts run top-level statements directly — no main function needed in a single-file program (an app's entry point uses @main). print adds the newline, like println. The languages will keep feeling this close: both were designed in the 2010s to replace an older platform language, and they arrived at strikingly similar answers.
val/var is let/var
fun main() { val fixed = 1 var counter = 0 counter += 1 println(fixed + counter) }
let fixed = 1 var counter = 0 counter += 1 print(fixed + counter)
A direct rename: Kotlin's val is Swift's let, and var is var. Both compilers infer types and nudge you toward the immutable spelling (Swift warns when a var never mutates). One deeper difference hides here: on a struct, Swift's let freezes the whole value, properties included — the structs section returns to this.
String interpolation
fun main() { val name = "world" println("Hello, $name! The answer is ${6 * 7}.") }
let name = "world" print("Hello, \(name)! The answer is \(6 * 7).")
Kotlin's $name/${…} becomes Swift's single \(…) form, which takes any expression. Both are compile-checked, both call the value's string conversion, and both support multi-line strings with triple quotes — the twins at their most identical.
Optionals: Twins, Almost
T? on both sides
Both languages solved null the same visible way: String?, safe chaining with ?., and a default operator — Kotlin's elvis ?: is Swift's ??.
fun main() { val name: String? = null println(name?.length ?: 0) val nickname: String? = "Ada" println(nickname?.length ?: 0) }
let name: String? = nil print(name?.count ?? 0) let nickname: String? = "Ada" print(nickname?.count ?? 0)
Same syntax, different machinery underneath: Kotlin's String? is a compiler-tracked nullable reference, while Swift's is sugar for Optional<String> — a real enum of .some(value)/.none, which is why optionals pattern-match in switch and why the unwrapping idioms in the next two concepts bind new names instead of smart-casting old ones.
Smart casts become if let
fun lengthOf(text: String?): Int { if (text != null) { return text.length } return 0 } fun main() { println(lengthOf("hello")) println(lengthOf(null)) }
func lengthOf(_ text: String?) -> Int { if let text { return text.count } return 0 } print(lengthOf("hello")) print(lengthOf(nil))
Kotlin's flow analysis retypes the same name after a null check; Swift's if let binds an unwrapped copy — and since Swift 5.7 the shorthand if let text (no = text) shadows the optional with its unwrapped self, making the two languages read almost identically at last.
?: return becomes guard let
Kotlin's early-exit idiom val name = maybeName ?: return is a first-class statement in Swift: guard let, the signature Swift control-flow keyword.
fun describe(maybeName: String?): String { val name = maybeName ?: return "anonymous" return "Hello, $name!" } fun main() { println(describe("Ada")) println(describe(null)) }
func describe(_ maybeName: String?) -> String { guard let name = maybeName else { return "anonymous" } return "Hello, \(name)!" } print(describe("Ada")) print(describe(nil))
guard inverts the condition — state what must be true, handle the failure in else, and continue with name bound for the rest of the scope. The compiler enforces that the else block exits (return, throw, continue), exactly like Rust's let-else. Swift code leans on guard heavily; expect to read functions as a stack of preconditions followed by a happy path.
!! is a single !
fun main() { val name: String? = "Ada" println(name!!.length) // (null)!! would throw NullPointerException }
let name: String? = "Ada" print(name!.count) // nil! would crash: "Unexpectedly found nil while // unwrapping an Optional value"
Kotlin makes the dangerous operator ugly on purpose (!!); Swift's is a single character, plus a whole family: as! force-casts and String! declares an implicitly-unwrapped optional (common in UI code, where lifecycle guarantees a value). The cultural advice matches Kotlin's: every ! is a claim the compiler cannot check — prefer if let, guard let, or ??.
Functions & Closures
Named arguments become required labels
Kotlin's named arguments are optional sugar. Swift flips the default: every parameter has an argument label the caller must write — omitting labels is the opt-in.
fun greet(name: String = "world", punctuation: String = "!") = "Hello, $name$punctuation" fun main() { println(greet()) println(greet(punctuation = "?")) println(greet(name = "Ada")) }
func greet(name: String = "world", punctuation: String = "!") -> String { "Hello, \(name)\(punctuation)" } print(greet()) print(greet(punctuation: "?")) print(greet(name: "Ada"))
Default values work identically, but the labels are part of the function's name — this function is greet(name:punctuation:). A label can differ from the internal name (func move(to destination: Point) reads "move to" at the call site) or be suppressed with _, which is why earlier examples declared func describe(_ maybeName:). API design in Swift is largely label design.
Trailing lambdas, trailing closures
Both languages let a final lambda argument escape the parentheses, and both give it implicit parameter names — Kotlin's it, Swift's $0.
fun main() { val doubled = listOf(1, 2, 3).map { it * 2 } println(doubled) val described = listOf(1, 2, 3).map { number -> "#$number" } println(described) }
let doubled = [1, 2, 3].map { $0 * 2 } print(doubled) let described = [1, 2, 3].map { number in "#\(number)" } print(described)
Swift's $0, $1… scale to several parameters where Kotlin's it stops at one, and named parameters use { number in … } for Kotlin's { number -> … }. The trailing-closure habit that powers Kotlin DSLs powers SwiftUI the same way — both platforms bet their UI frameworks on this one syntax.
Structs Are Values
Assignment copies a struct
The deepest difference between the twins. A Kotlin data class is a reference — two names, one object. A Swift struct is a value — assignment copies, and the copies are independent.
data class Point(var x: Int, var y: Int) fun main() { val first = Point(1, 2) val second = first second.x = 99 println(first.x) }
struct Point { var x: Int var y: Int } var first = Point(x: 1, y: 2) var second = first second.x = 99 print(first.x) print(second.x)
Kotlin prints 99 — mutating through second changed the one shared object. Swift prints 1 then 99: second is a genuine copy. Swift's standard library is built almost entirely from structs (including String, Array, Dictionary), and the memberwise initializer Point(x:y:) comes free, like a data class constructor. "When in doubt, make it a struct" is the Swift default; Kotlin has no equivalent choice to make.
mutating methods
Because structs are values, a method that changes one must say so: mutating — and it is a compile error to call it on a let struct.
class Counter { var count = 0 private set fun increment() { count += 1 } } fun main() { val counter = Counter() counter.increment() counter.increment() println(counter.count) }
struct Counter { private(set) var count = 0 mutating func increment() { count += 1 } } var counter = Counter() counter.increment() counter.increment() print(counter.count) // let frozen = Counter(); frozen.increment() ← compile error
Kotlin's val counter holds a reference, so the object behind it mutates freely. Swift's let counter would freeze the entire value — var is required to call increment(). The mutating keyword makes a struct's API honest about which methods change state, information a Kotlin class signature never carries. private(set) is a direct twin of Kotlin's private set.
Classes still exist — for reference semantics
Swift keeps classes for identity and shared mutable state — they behave like Kotlin's. The choice struct-or-class is a semantic decision Kotlin never asks you to make.
class Session(var user: String) fun main() { val session = Session("ada") val alias = session alias.user = "grace" println(session.user) }
final class Session { var user: String init(user: String) { self.user = user } } let session = Session(user: "ada") let alias = session alias.user = "grace" print(session.user)
Both print grace — with a class, alias and session share one object, and note that Swift's let only pins the reference, exactly like val. Classes need a written init (no memberwise freebie), support inheritance unless marked final, and participate in ARC (last section). Rule of thumb: structs for data, classes for identity — model objects with lifecycles, shared caches, anything where "the same one" matters.
Collections
let/var replaces List/MutableList
Kotlin splits mutability across interfaces (List vs MutableList). Swift has one Array, one Dictionary — the binding keyword decides.
fun main() { val readOnly = listOf(1, 2, 3) val growable = mutableListOf(1, 2, 3) growable.add(4) println(growable) val ages = mutableMapOf("Ada" to 36) ages["Grace"] = 45 println(ages["Ada"] ?: 0) }
let readOnly = [1, 2, 3] var growable = [1, 2, 3] growable.append(4) print(growable) var ages = ["Ada": 36] ages["Grace"] = 45 print(ages["Ada"] ?? 0)
A let array is deeply immutable — no append, no element assignment, checked at compile time — which is stronger than Kotlin's listOf (a read-only view that other code holding the mutable reference can still change). Dictionary subscripts return optionals, so the whole optionals toolkit (??, if let) applies to lookups, mirroring Kotlin's nullable map[key].
Collections are values too
Arrays and dictionaries are structs, so the value-semantics lesson applies to them: assignment copies the collection.
fun main() { val first = mutableListOf(1, 2, 3) val second = first second.add(4) println(first) }
var first = [1, 2, 3] var second = first second.append(4) print(first) print(second)
Kotlin prints [1, 2, 3, 4] — the aliasing again. Swift prints [1, 2, 3] and [1, 2, 3, 4]: independent values. Copy-on-write makes this affordable — the copy shares storage until one side mutates — so you get aliasing-bug immunity without paying for copies you never observe. An entire category of "defensive copy" code from the JVM world simply does not exist in Swift.
map/filter chains, both eager
fun main() { val result = listOf(1, 2, 3, 4, 5, 6) .filter { it % 2 == 0 } .map { it * it } .sum() println(result) }
let result = [1, 2, 3, 4, 5, 6] .filter { $0 % 2 == 0 } .map { $0 * $0 } .reduce(0, +) print(result)
The chains translate symbol for symbol, and both run eagerly, building intermediate arrays — with the same opt-in laziness: Kotlin's asSequence() is Swift's .lazy. Swift has no sum(); reduce(0, +) passes the + operator itself as the combining function, a small taste of operators being ordinary functions.
Enums & Pattern Matching
Sealed classes are enums with associated values
Kotlin models a closed set of shapes with a sealed hierarchy. Swift's enum does it in one declaration — each case carrying its own payload.
sealed class Shape data class Circle(val radius: Double) : Shape() data class Rectangle(val width: Double, val height: Double) : Shape() fun area(shape: Shape): Double = when (shape) { is Circle -> 3.14159 * shape.radius * shape.radius is Rectangle -> shape.width * shape.height } fun main() { println(area(Rectangle(3.0, 4.0))) }
enum Shape { case circle(radius: Double) case rectangle(width: Double, height: Double) } func area(of shape: Shape) -> Double { switch shape { case .circle(let radius): return 3.14159 * radius * radius case .rectangle(let width, let height): return width * height } } print(area(of: .rectangle(width: 3, height: 4)))
Where Kotlin's when smart-casts each subclass, Swift's switch destructures the case's payload straight into bindings — and the leading-dot shorthand (.rectangle(…)) omits the type when the compiler already knows it. Both enforce exhaustiveness over the closed set. Swift enums also carry methods, computed properties, and generics; Optional itself is one.
when guards become where
fun describe(number: Int): String = when { number == 0 -> "zero" number < 0 -> "negative" number % 2 == 0 -> "positive even" else -> "positive odd" } fun main() { println(describe(-4)) }
func describe(_ number: Int) -> String { switch number { case 0: return "zero" case let value where value < 0: return "negative" case let value where value % 2 == 0: return "positive even" default: return "positive odd" } } print(describe(-4))
case let value where … binds and tests in one pattern — Kotlin's subjectless when ladder expressed as guarded patterns. Swift's switch also matches ranges (case 1...9: for Kotlin's in 1..9), tuples, and enum payloads, never falls through, and demands exhaustiveness like when over a sealed type. Its one gap from Kotlin: switch is a statement historically — expression forms arrived only recently and are still limited.
Protocols & Extensions
Interfaces become protocols
interface Speaker { fun speak(): String } class Dog : Speaker { override fun speak() = "Woof!" } fun main() { val speakers: List<Speaker> = listOf(Dog()) println(speakers.first().speak()) }
protocol Speaker { func speak() -> String } struct Dog: Speaker { func speak() -> String { "Woof!" } } let speakers: [any Speaker] = [Dog()] print(speakers[0].speak())
Conformance is declared (struct Dog: Speaker) as in Kotlin — Swift is not structurally typed like Go — but structs, enums, and classes can all conform, and no override keyword marks the implementations. The any Speaker spelling makes the boxed, dynamically-dispatched existential explicit (Swift 5.6+), a cost Kotlin's interfaces carry invisibly.
Extension functions are extensions
Kotlin's single extension function becomes a block: extension String { } can add methods, computed properties, initializers, and protocol conformances.
fun String.shouted(): String = uppercase() + "!" fun main() { println("hello".shouted()) }
extension String { func shouted() -> String { uppercased() + "!" } } print("hello".shouted())
The mechanism both languages share: static resolution, no monkey-patching, imports control visibility. Swift's version does more — an extension can conform an existing type to a protocol (extension String: MyProtocol), which Kotlin cannot express at all, and Swift's own standard library is organized as protocol-plus-extensions rather than base classes.
Protocol extensions: default methods, upgraded
Kotlin interfaces can hold default method bodies. Swift moves them into protocol extensions — the heart of "protocol-oriented programming."
interface Greeter { val name: String fun greet(): String = "Hello, $name!" } class FriendlyBot(override val name: String) : Greeter fun main() { println(FriendlyBot("Kotlin").greet()) }
protocol Greeter { var name: String { get } } extension Greeter { func greet() -> String { "Hello, \(name)!" } } struct FriendlyBot: Greeter { let name: String } print(FriendlyBot(name: "Swift").greet())
The protocol declares only requirements; the extension supplies shared behavior to every conformer, including retroactively and constrained (extension Collection where Element: Equatable). This is Swift's answer to base classes — the 2015 "Protocol-Oriented Programming" talk that introduced it reshaped the language's whole style, and it is why Swift code inherits far less than Kotlin code.
Properties & Statics
Computed properties
class Circle(val radius: Double) { val area: Double get() = 3.14159 * radius * radius } fun main() { println(Circle(2.0).area) }
struct Circle { var radius: Double var area: Double { 3.14159 * radius * radius } } print(Circle(radius: 2).area)
Kotlin's val area get() = … is Swift's braced computed property; both recompute on access and neither stores anything. Swift adds property observers (willSet/didSet) on stored properties and lazy var for Kotlin's by lazy { } — the property toolbox is near-identical once the spellings are mapped.
companion object becomes static
class Temperature private constructor(val celsius: Double) { companion object { val BOILING = Temperature(100.0) fun fromFahrenheit(value: Double) = Temperature((value - 32) / 1.8) } } fun main() { println(Temperature.BOILING.celsius) println(Temperature.fromFahrenheit(212.0).celsius) }
struct Temperature { let celsius: Double static let boiling = Temperature(celsius: 100) static func fromFahrenheit(_ value: Double) -> Temperature { Temperature(celsius: (value - 32) / 1.8) } } print(Temperature.boiling.celsius) print(Temperature.fromFahrenheit(212).celsius)
Swift kept the static keyword Kotlin removed, so there is no companion-object indirection — members belong to the type directly. Kotlin's object singletons map to a static let shared = … convention (see URLSession.shared across Apple's frameworks) rather than a language feature.
Errors: throws & defer
Exceptions become marked throws
Swift errors look like exceptions but behave like checked return values: a function declares throws, and every call site must write try — invisible propagation is impossible.
fun safeDivide(numerator: Double, denominator: Double): Double { require(denominator != 0.0) { "cannot divide by zero" } return numerator / denominator } fun main() { try { println(safeDivide(10.0, 2.0)) println(safeDivide(1.0, 0.0)) } catch (exception: IllegalArgumentException) { println(exception.message) } }
enum DivisionError: Error { case divideByZero } func safeDivide(_ numerator: Double, by denominator: Double) throws -> Double { guard denominator != 0 else { throw DivisionError.divideByZero } return numerator / denominator } do { print(try safeDivide(10, by: 2)) print(try safeDivide(1, by: 0)) } catch DivisionError.divideByZero { print("cannot divide by zero") }
Three honesty rules Kotlin lacks: the signature says throws, the call says try, and a non-throwing function cannot let an error slip through. Error types are ordinary values conforming to Error — enums, usually — and catch pattern-matches them like a switch. It is the middle road between Kotlin's unchecked exceptions and Rust's fully-explicit Result.
try? and try!
Swift folds its error system into its optional system: try? converts a thrown error into nil, and try! asserts success.
fun parseOrNull(text: String): Int? = try { text.toInt() } catch (exception: NumberFormatException) { null } fun main() { println(parseOrNull("42") ?: 0) println(parseOrNull("oops") ?: 0) }
enum ParseError: Error { case notANumber } func parse(_ text: String) throws -> Int { guard let number = Int(text) else { throw ParseError.notANumber } return number } print((try? parse("42")) ?? 0) print((try? parse("oops")) ?? 0)
Kotlin needs a helper to turn an exception into a null (or runCatching { }.getOrNull()); Swift makes it one keyword, and the result chains straight into ??. try! is the force-unwrap of the error world — crash on failure — with the same code-review eyebrows as !.
try/finally becomes defer
fun work() { println("opening database") try { println("using database") } finally { println("closing database") } } fun main() { work() println("after the function") }
func work() { print("opening database") defer { print("closing database") } print("using database") } work() print("after the function")
Like Go's defer — cleanup declared beside acquisition, run on every exit path including thrown errors — but scoped to the enclosing block rather than the whole function, and multiple defers run in reverse order. It replaces both finally and most use { } patterns; ARC (last section) replaces the rest.
async/await & Tasks
suspend becomes async
Both languages chose colored functions with structured concurrency: Kotlin's suspend fun is Swift's async func, and both mark the call site.
import kotlinx.coroutines.* suspend fun compute(number: Int): Int { delay(10) return number * number } fun main() = runBlocking { println(compute(6)) }
func compute(_ number: Int) async -> Int { try? await Task.sleep(nanoseconds: 10_000_000) return number * number } let squared = await compute(6) print(squared)
The rename table: suspendasync, and where Kotlin marks nothing at the call site (suspension points are invisible), Swift requires await on every one — you can see exactly where a function might pause. Swift scripts may await at top level; Kotlin needs the runBlocking bridge. Underneath, both compile to continuations scheduled on cooperative thread pools.
coroutineScope becomes TaskGroup
Structured concurrency on both sides: children belong to a scope that cannot leak them, and cancellation flows down the tree.
import kotlinx.coroutines.* suspend fun compute(number: Int): Int { delay(10) return number * number } fun main() = runBlocking { val results = coroutineScope { (1..4).map { number -> async { compute(number) } }.awaitAll() } println(results) }
func compute(_ number: Int) async -> Int { number * number } let results = await withTaskGroup(of: Int.self) { group in for number in 1...4 { group.addTask { await compute(number) } } var collected: [Int] = [] for await value in group { collected.append(value) } return collected.sorted() } print(results)
Kotlin's async { }/awaitAll() is Swift's group.addTask { }/for await — same guarantee that the scope outlives its children, same automatic cancellation propagation. One Swift difference shows in the sorted(): group results arrive in completion order, not submission order. For two known-arity tasks, async let is the lighter spelling. Swift 6 adds compile-time data-race checking on top — Sendable is its Send/Sync.
ARC vs GC
Deterministic deinit
The platform-level difference under everything: the JVM garbage-collects on its own schedule; Swift reference-counts (ARC), so an object dies at a known moment — and deinit runs right there.
class Connection(val name: String) : AutoCloseable { init { println("opening $name") } override fun close() = println("closing $name") // No deterministic destructor exists; a finalizer // may run late or never — hence use { } and close(). } fun main() { Connection("database").use { connection -> println("using ${connection.name}") } println("after the block") }
final class Connection { let name: String init(name: String) { self.name = name print("opening \(name)") } deinit { print("closing \(name)") } } do { let connection = Connection(name: "database") print("using \(connection.name)") } print("after the scope")
Kotlin needs the AutoCloseable/use contract because the GC gives no timing promise; Swift's deinit fires the instant the last reference drops — resource cleanup without a protocol. The tax is reference cycles: two objects holding each other never hit zero, so Swift code writes weak/unowned (especially in closures capturing self) where the JVM's tracing collector would just collect the pair. No GC pauses, but a new class of leak to learn.