PONYλM2Modula-2

Swift.CodeCompared.To/Scala

An interactive executable cheatsheet comparing Swift and Scala

Swift 6.3 Scala 3.4
Basics & Syntax
Hello, World
Scala 2 had no top-level definitions, so the program lives in an object. Scala 3 lifted that restriction, and the examples here keep the explicit form because it is what the browser compiles.
print("Hello, World!")
object Main { def main(args: Array[String]): Unit = { println("Hello, World!") } }
Scala 2 has no top-level definitions, so the entry point lives in an object — a singleton, which is Swift's static members with a name you can pass around. func is def, Void is Unit, and semicolons are optional. Scala 3 adds top-level definitions and an @main annotation, which collapses this to almost exactly the Swift form.
let and var become val and var
The correspondence is exact and the default is the same: val is let, var is var, and both languages want you reaching for the first one.
let fixed = 1 var counter = 0 counter += 1 let explicit: Int64 = 42 let name = "Ada" print("\(name): \(fixed + counter), \(explicit)") // lazy: computed once, on first access. (It has to live in a type — a top-level // global in Swift is already lazy, so the keyword is redundant there.) struct Cache { lazy var expensive: String = { print("computing") return "cached" }() } var cache = Cache() print(cache.expensive) print(cache.expensive)
object Main { def main(args: Array[String]): Unit = { val fixed = 1 // let var counter = 0 // var counter += 1 val explicit: Long = 42 val name = "Ada" // s"..." is the interpolator. The prefix names it — and you can define // your own, which is how compile-time-checked sql"..." literals work. println(s"$name: ${fixed + counter}, $explicit") println(f"${1.0 / 3}%.2f") // f: printf, type-checked against the arguments // lazy val is a keyword, not a closure trick: memoized and thread-safe. lazy val expensive: String = { println("computing") "cached" } println(expensive) println(expensive) } }
Immutable by default in both, with the same two keywords — let is simply spelled val. Two small upgrades: lazy val is a language keyword rather than Swift's lazy var-with-a-closure (and it is memoized and thread-safe), and string interpolators are extensibles"…" is a method call on StringContext, so a library can add sql"…" or json"…" that validate at compile time. Swift has no equivalent.
Interpolation Needs a Prefix
Swift interpolates in every string literal. Scala only interpolates in a literal you have prefixed with s, f or raw — and that prefix is an ordinary method you could write yourself.
import Foundation let name = "widget" let count = 7 print("\(count) x \(name)") print(String(format: "%.2f", 12.5)) print(""" multi line """)
object Main { def main(args: Array[String]): Unit = { val name = "widget" val count = 7 println(s"$count x $name") // s for simple println(f"${12.5}%.2f") // f for formatted, checked at COMPILE time println( """|multi |line""".stripMargin) } }
The f interpolator is the one worth knowing: f"${12.5}%.2f" is checked at compile time, so a %d against a String will not build — where Swift's String(format:) is a run-time call that will happily print garbage. Scala's triple-quoted string does not strip indentation on its own, which is why .stripMargin and the | markers are there; Swift measures the indentation of the closing delimiter instead. And because an interpolator is just a method on StringContext, a library can add its own — sql"…" and json"…" are real and have no Swift counterpart.
Everything Is an Expression
🚨 The habit to unlearn first. In Scala if, match, try and a block all have a value, so most functions need no return at all — and writing one is a code smell rather than a style choice.
func classify(_ number: Int) -> String { // Swift's if is a STATEMENT, so this needs a return in each branch // or a ternary. if number < 0 { return "negative" } else if number == 0 { return "zero" } else { return "positive" } } print(classify(-3), classify(0), classify(9))
object Main { // The if IS the value, and the last expression of a block is its result. def classify(number: Int): String = if number < 0 then "negative" else if number == 0 then "zero" else "positive" def main(args: Array[String]): Unit = { // A block is an expression too, so this is a value. val loud = { val parts = List(-3, 0, 9).map(classify) parts.mkString(" ") } println(loud) } }
Swift has one expression form of this — the ternary — and Scala needs none because the ordinary if already is one. The same holds for match, which is where it pays off: a Swift switch assigning to a variable declared just above it becomes a val x = value match { … } with no mutable step in between. Scala 3 also made then and the parentheses optional, which is why the two branches read as one expression rather than as a block of statements.
Optionals & Option
Optional<T> becomes Option[T] — a value, not syntax
🚨 The deepest difference on the page. Swift builds optionality into the language with ?, ! and if let; Scala's Option[T] is an ordinary type in an ordinary library.
func findName(_ id: Int) -> String? { id == 1 ? "Ada" : nil } // The optional has dedicated SYNTAX: ?. and ?? and ! and if let. print(findName(1)?.uppercased() ?? "(none)") print(findName(1)!) if let name = findName(1) { print(name.count) } func shout(_ id: Int) -> String { guard let name = findName(id) else { return "(nobody)" } return name.uppercased() } print(shout(99)) let names = ["Ada", nil, "Grace"] print(names.compactMap { $0 })
object Main { // Option[String] is Some(value) or None: an ordinary two-case sealed type from // the standard library, NOT a compiler feature. So the syntax becomes METHODS. def findName(id: Int): Option[String] = if (id == 1) Some("Ada") else None def shout(id: Int): String = findName(id) match { case Some(name) => name.toUpperCase case None => "(nobody)" } def main(args: Array[String]): Unit = { println(findName(1).map(_.toUpperCase).getOrElse("(none)")) // ?. and ?? println(findName(1).get) // the force unwrap println(findName(99).fold("(none)")(_.toUpperCase)) // both cases at once findName(1).foreach(name => println(name.length)) // if let println(shout(99)) val names = List(Some("Ada"), None, Some("Grace")) println(names.flatten) // compactMap println(names.flatMap(_.map(_.toUpperCase))) } }
Both languages solve the same problem; Swift puts absence in the syntax and Scala puts it in a value. Because Option is an ordinary type, the operators become methods — map for ?., getOrElse for ??, get for !, foreach for if let — and it composes with everything else: it goes in a List, it is pattern-matched, it flows through a for-comprehension, and the same map/flatMap vocabulary works on List, Either, Try and Future. The cost is an allocation per Some (Swift's ? is erased and free), and the fact that null still exists underneath on the JVM — idiomatic Scala wraps it immediately with Option(javaValue).
Optional Chaining Becomes map and flatMap
Swift's ?., ?? and if let are three pieces of syntax. Scala has none of them, because Option is a one-element collection and the collection methods already do the work.
struct User { let nickname: String? } let users: [User] = [User(nickname: "ada"), User(nickname: nil)] for user in users { print(user.nickname?.uppercased() ?? "anonymous") } // Chaining something that can ALSO fail needs flatMap. let text: String? = "42" print(text.flatMap { Int($0) }.map { $0 * 2 } ?? -1)
case class User(nickname: Option[String]) object Main { def main(args: Array[String]): Unit = { val users = List(User(Some("ada")), User(None)) for (user <- users) { // map is ?. and getOrElse is ?? println(user.nickname.map(_.toUpperCase).getOrElse("anonymous")) } val text: Option[String] = Some("42") println(text.flatMap(_.toIntOption).map(_ * 2).getOrElse(-1)) } }
The rule that makes the rest obvious: map when the function cannot fail, flatMap when it can — otherwise you get an Option[Option[Int]], which is exactly what Swift's ?. flattens for you silently. What Scala gains by having no syntax is that everything Option can do, List, Either, Try and Future can do with the same names; ?. works on optionals and on nothing else. fold, filter, exists and collect are all available on an Option and have no Swift equivalent at all.
compactMap Becomes flatten
Swift needs a purpose-built compactMap to drop the nils. Scala needs nothing new: an Option already behaves as a list of zero or one item, so flattening does it.
let inputs = ["1", "two", "3"] let numbers = inputs.compactMap { Int($0) } print(numbers) // Nothing at all was parseable? print(inputs.compactMap { Int($0) }.isEmpty) // The first success, without parsing the rest. print(inputs.lazy.compactMap { Int($0) }.first ?? -1)
object Main { def main(args: Array[String]): Unit = { val inputs = List("1", "two", "3") // flatMap over a function returning Option: each None contributes // nothing, each Some contributes its one element. val numbers = inputs.flatMap(_.toIntOption) println(numbers) println(inputs.flatMap(_.toIntOption).isEmpty) // collectFirst stops at the first match. println(inputs.flatMap(_.toIntOption).headOption.getOrElse(-1)) } }
This is the payoff for Option being a type rather than syntax. List[Option[A]] flattens to List[A] because an Option is implicitly viewable as an Iterable — so flatten, flatMap and a for comprehension all work on it without anybody having written a special case. The output differs in one place worth noticing: Scala prints List(1, 3) and Swift prints [1, 3], which is the collection's own description rather than anything about the operation.
Value Semantics & Case Classes
struct becomes case class — with a caveat
A case class gives you what a Swift struct gives you — equality, a constructor, a description, destructuring — and it is still a reference type underneath.
struct Person: Equatable { let name: String let age: Int func greeting() -> String { "Hi, \(name)" } } var ada = Person(name: "Ada", age: 36) var copied = ada // a COPY: struct is a value type print(ada) print(ada == Person(name: "Ada", age: 36)) // structural equality, free print(ada.greeting()) // Destructuring is not built in; you read the properties. print(ada.name, ada.age)
case class Person(name: String, age: Int) { def greeting: String = s"Hi, $name" // a zero-arg method may omit the parens } object Main { def main(args: Array[String]): Unit = { val ada = Person("Ada", 36) println(ada) // toString, generated println(ada == Person("Ada", 36)) // equals/hashCode, generated println(ada.greeting) println(ada.copy(age = 37)) // copy-with-changes, generated // A case class DESTRUCTURES, because it generates an unapply method. val Person(name, age) = ada println(s"$name $age") // The caveat: a case class is a CLASS — a reference type. Assignment does // NOT copy. It is safe anyway, because the fields are 'val' (immutable), so // sharing cannot hurt you and copy() is the only way to "change" one. val alias = ada println(alias eq ada) // true: the SAME object println(ada.copy(name = "Alan") eq ada) // false: a new one } }
A case class gives you what a Swift struct gives you — generated equality, a readable toString, and (better than Swift) a copy(field = value) expression and destructuring via unapply. The caveat is that it is a reference type: val alias = ada shares the object rather than copying it. That is safe in practice because case-class fields are val by default, so immutability does the job value semantics do in Swift — but the moment you write var in a case class, you have a shared mutable object and Swift's guarantee is gone. The Scala convention is exactly the Swift habit: keep them immutable.
var Field Becomes copy()
🚨 The practical consequence of a case class being a reference type: there is no mutating a field on a copy, because there is no copy. You build a new value with copy().
struct Point: Equatable { var x: Int; var y: Int } var first = Point(x: 1, y: 2) var second = first // a genuine copy — assignment copies the value second.x = 99 print(first.x, second.x) print(first == second)
case class Point(x: Int, y: Int) object Main { def main(args: Array[String]): Unit = { val first = Point(1, 2) val second = first // the SAME object; both names point at it // So a change is a new value, not a mutation. val moved = second.copy(x = 99) println(s"${first.x} ${moved.x}") println(first == second) // true — case classes compare by value println(first eq second) // true — and they are literally one object } }
Because nothing was copied, nothing had to be: first and second are one immutable object, and copy(x = 99) allocates the only new one in the example. That is the same optimization Swift's copy-on-write performs for Array, arrived at from the other direction — Scala makes the value immutable so sharing is always safe, where Swift copies eagerly and defers the work. 🚨 A case class with a var field is legal and throws all of this away: == keeps comparing by value while the value can change underneath, so it will not behave in a Set or as a Map key.
The Collections Are Immutable Too
Swift's let makes an array unchangeable. Scala's val only fixes the binding — but the collection it names is an immutable type to begin with, and the mutable ones need an import.
let fixed = [1, 2, 3] // fixed.append(4) — will not compile: fixed is a let var growing = [1, 2, 3] growing.append(4) print(growing) // The immutability is in the BINDING, so a var array is fully mutable. var lookup = ["a": 1] lookup["b"] = 2 print(lookup.count)
object Main { def main(args: Array[String]): Unit = { val fixed = List(1, 2, 3) // fixed has no append — the TYPE has no mutating operation at all. val grown = fixed :+ 4 // a new list println(grown) println(fixed) // unchanged // A mutable collection is a different type, and it is opt-in. val lookup = scala.collection.mutable.Map("a" -> 1) lookup("b") = 2 println(lookup.size) } }
The distinction is worth holding onto because the two languages put it in different places. Swift puts it on the binding — one Array type, mutable or not depending on let versus var. Scala puts it on the typeList has no append to call, so a value of that type is safe to share with anything, including another thread, no matter who holds the binding. That is why val on a mutable map still lets you change the map: the binding is fixed and the object is not.
Enums & Sealed Traits
Enums with associated values become enums
Both languages have an enum whose cases carry payloads, and the exhaustiveness check behind them is not the same strength.
enum Shape { case circle(radius: Double) case rectangle(width: Double, height: Double) case point } func area(_ shape: Shape) -> Double { switch shape { // exhaustive: a missing case will not compile case .circle(let radius): return 3.14159 * radius * radius case .rectangle(let width, let height): return width * height case .point: return 0 } } print(area(.circle(radius: 1))) print(area(.rectangle(width: 2, height: 3))) print(area(.point))
enum Shape { case Circle(radius: Double) case Rectangle(width: Double, height: Double) case Point // no payload, so a singleton } object Main { import Shape.* // The compiler knows every case, so it checks exhaustiveness — as a // WARNING, not an error. See the note. def area(shape: Shape): Double = shape match { case Circle(radius) => 3.14159 * radius * radius case Rectangle(width, height) => width * height case Point => 0.0 } def main(args: Array[String]): Unit = { println(area(Circle(1.0))) println(area(Rectangle(2.0, 3.0))) println(area(Point)) } }
These are the same declaration in two languages: a case with a payload becomes a case with parameters, a case without one stays a bare name, and match destructures exactly as switch does. The only syntactic tax is import Shape.* — Scala scopes the cases inside the enum, so a match either imports them or writes Shape.Circle each time, where Swift lets you lead with a bare dot. One real difference remains, and it is a downgrade: an inexhaustive match is a warning in Scala, not an error, so it compiles and throws MatchError at run time. Every serious Scala build turns on -Xfatal-warnings, which restores the guarantee you take for granted.
Cases, Values, and Methods on an Enum
Both languages let a payload-free enum list its own cases and carry methods, and the spellings are close enough to guess.
enum Direction: String, CaseIterable { case north, south, east, west var opposite: Direction { switch self { case .north: return .south case .south: return .north case .east: return .west case .west: return .east } } } for direction in Direction.allCases { print(direction.rawValue, direction.opposite.rawValue) }
enum Direction { case North, South, East, West def opposite: Direction = this match { case North => South case South => North case East => West case West => East } } object Main { def main(args: Array[String]): Unit = { // values is CaseIterable, and every case knows its own position. for (direction <- Direction.values) { println(s"${direction} ${direction.opposite} ${direction.ordinal}") } } }
Scala's values is allCases, ordinal is the position with no Swift equivalent, and Direction.valueOf("North") is Direction(rawValue:). The difference underneath is that a Scala 3 enum is sugar for a sealed hierarchy of objects, so each case is a real singleton you can pattern-match on, extend with a trait, and hold in a Map — while a Swift enum without associated values is fundamentally an integer with names. That is also why Scala's cases are capitalized: they are types.
Pattern Matching
Patterns you can define yourself
Swift's switch matches types, values, and tuples. Scala's match matches anything you teach it to — because a pattern is just an object with an unapply method.
func describe(_ text: String) -> String { // Swift can only match on types and constants, so any PARSING has to happen // before the switch, in a variable. if let number = Int(text) { return number > 100 ? "big number \(number)" : "number \(number)" } if text.hasPrefix("#") { return "tag \(text.dropFirst())" } return "text \(text)" } print(describe("7")) print(describe("1000")) print(describe("#swift")) print(describe("hello"))
import scala.util.Try // An EXTRACTOR: unapply turns a String into an Option[Int]. Some => the pattern // matched and the contents are bound; None => try the next case. object AsInt { def unapply(text: String): Option[Int] = Try(text.toInt).toOption } object Tag { def unapply(text: String): Option[String] = if (text.startsWith("#")) Some(text.drop(1)) else None } object Main { // The parsing now happens INSIDE the pattern, and binds its result. def describe(text: String): String = text match { case AsInt(number) if number > 100 => s"big number $number" // if = Swift's where case AsInt(number) => s"number $number" case Tag(name) => s"tag $name" case other => s"text $other" } def main(args: Array[String]): Unit = { println(describe("7")) println(describe("1000")) println(describe("#scala")) println(describe("hello")) // Lists match by SHAPE, with a cons pattern Swift has no version of. List(1, 2, 3) match { case head :: tail => println(s"$head then $tail") case Nil => println("empty") } } }
Any object with unapply(input): Option[Result] can appear on the left of a case, which makes pattern matching open: you can match on a parsed integer, a regex capture, a JSON shape, or a domain concept like case ValidEmail(user, host), and it reads as though the language knew about it. Swift's switch is closed by comparison — it matches types, constants and conditions, so real parsing must be lifted out into a variable first, exactly as the left column does. Scala also has the cons pattern (head :: tail), which makes recursive list code read the way the textbooks draw it.
Matching on Type, and on a Condition
Swift's case let x as Type and where become Scala's case x: Type and if, and the two read almost identically.
func describe(_ value: Any) -> String { switch value { case let number as Int where number < 0: return "a negative int" case let number as Int: return "the int \(number)" case let text as String: return "a string of \(text.count)" case is Double: return "some double" default: return "something else" } } for value in [-1, 7, "hello", 2.5, true] as [Any] { print(describe(value)) }
object Main { def describe(value: Any): String = value match { case number: Int if number < 0 => "a negative int" case number: Int => s"the int $number" case text: String => s"a string of ${text.length}" case _: Double => "some double" case _ => "something else" } def main(args: Array[String]): Unit = { List(-1, 7, "hello", 2.5, true).foreach(value => println(describe(value))) } }
🚨 Both of these are unsound in the same way, and for different reasons. Swift's default is required because Any is open; Scala's case _ is required for the same reason, and worse, case list: List[Int] would match a List[String] too — the element type is erased at run time and the compiler warns about exactly that. Where Scala pulls ahead is that a match arm is not limited to types: case Point(0, y), case head :: tail, case n if n % 3 == 0 and a user-defined extractor all sit in the same list of arms.
Destructuring, and the @ Binding
Tuple and structure destructuring look the same in both languages. Scala's @ — bind this name to the whole thing I just matched — is the piece Swift has no spelling for.
struct Point { let x: Int; let y: Int } let points = [Point(x: 0, y: 5), Point(x: 3, y: 4)] for point in points { switch (point.x, point.y) { case (0, let y): print("on the axis at \(y)") case (let x, let y): // Swift cannot name the WHOLE match, so the parts are reassembled. print("at \(x),\(y)") } }
case class Point(x: Int, y: Int) object Main { def main(args: Array[String]): Unit = { val points = List(Point(0, 5), Point(3, 4)) for (point <- points) { point match { case Point(0, y) => println(s"on the axis at $y") // whole @ pattern: destructure AND keep the original. case whole @ Point(x, y) => println(s"at $x,$y from $whole") } } } }
The @ earns its keep in a nested match, where the piece you want to keep is three levels down and reassembling it means writing the constructor call out again — which then has to be kept in step with the pattern above it. Scala also matches lists structurally (case first :: second :: rest) and can bind a name inside that, which is where Swift's array patterns stop entirely: there is no case [first, second, ...rest].
Protocols & Traits
Protocols become traits — with state, and stackable
A trait is a protocol that may also hold state and constructor logic, which is what lets several of them stack onto one class.
protocol Greeter { var name: String { get } func greet() -> String } // A protocol extension provides the default. extension Greeter { func greet() -> String { "Hello, \(name)" } } struct Person: Greeter { let name: String } print(Person(name: "Ada").greet()) // A protocol cannot store a property — the conformer must. // And there is no way to "stack" two protocol extensions of the same method: // the compiler would not know which one to pick.
trait Greeter { def name: String var greetings = 0 // a trait CAN hold state — Swift cannot def greet(): String = s"Hello, $name" } // 'super' inside a trait is not resolved until the trait is MIXED IN, which is // what makes traits stackable decorators. trait Loud extends Greeter { override def greet(): String = super.greet().toUpperCase + "!" } trait Counting extends Greeter { override def greet(): String = { greetings += 1 super.greet() } } class Person(val name: String) extends Greeter with Loud with Counting object Main { def main(args: Array[String]): Unit = { val person = new Person("Ada") println(person.greet()) // Counting runs first, then Loud — linearization, println(person.greet()) // right to left through the 'with' clauses println(person.greetings) } }
A trait is a protocol with two additions. It can hold state (var greetings), which a Swift protocol extension cannot — Swift protocols may declare a property but never store it. And super inside a trait is resolved at mix-in time, by linearizing the traits right to left, so with Loud with Counting makes each trait a composable decorator over the next. That is the "stackable traits" pattern, it has no Swift equivalent, and it is also the sharpest edge in the language: reorder the with clauses and the behavior changes silently.
Traits Stack, in a Defined Order
🚨 Two traits with the same method is an error in Swift and a feature in Scala: they compose, and super means the next one along a linearization the compiler computes.
protocol Logger { func log(_ message: String) } // Swift has no way for a default implementation to call "the next one". // Composition is the answer: wrap a logger in another logger. struct PlainLogger: Logger { func log(_ message: String) { print(message) } } struct Timestamped: Logger { let inner: Logger func log(_ message: String) { inner.log("[t] " + message) } } struct Shouting: Logger { let inner: Logger func log(_ message: String) { inner.log(message.uppercased()) } } Shouting(inner: Timestamped(inner: PlainLogger())).log("started")
trait Logger { def log(message: String): Unit = println(message) } // abstract override: "I modify whatever comes after me." trait Timestamped extends Logger { abstract override def log(message: String): Unit = super.log("[t] " + message) } trait Shouting extends Logger { abstract override def log(message: String): Unit = super.log(message.toUpperCase) } object Main { def main(args: Array[String]): Unit = { // Linearization runs RIGHT TO LEFT: Shouting, then Timestamped, then Logger. val logger = new Logger with Timestamped with Shouting logger.log("started") } }
The two columns print the same line and get there differently: Swift nests three objects by hand, Scala names three traits and the compiler builds the chain. The rule is that with reads right to left — the last trait named runs first and its super is the one before it — which is unintuitive exactly once and then never again. What you are buying is that the pieces need not know about each other; the cost is that a super call whose target is decided by the mixin order at every use site is genuinely harder to follow than a constructor argument you can see.
associatedtype Becomes an Abstract type
Swift's associatedtype and Scala's abstract type member are the same idea, and Scala additionally lets the same trait take the parameter the other way round.
protocol Container { associatedtype Item var items: [Item] { get } func first() -> Item? } struct Bag: Container { typealias Item = String let items: [String] func first() -> String? { items.first } } let bag = Bag(items: ["apple", "pear"]) print(bag.first() ?? "empty") // 🚨 An existential needs the type spelled out: let containers: [any Container] = [bag] print(containers.count)
trait Container { type Item // abstract type member def items: List[Item] def first: Option[Item] = items.headOption } class Bag(val items: List[String]) extends Container { type Item = String } object Main { def main(args: Array[String]): Unit = { val bag = new Bag(List("apple", "pear")) println(bag.first.getOrElse("empty")) // Container is an ordinary type, so a list of them needs no ceremony. val containers: List[Container] = List(bag) println(containers.size) } }
The visible difference is what happens when you put one in a collection. A Swift protocol with an associatedtype could not be used as a type at all until any Container arrived, and even now the associated type is hidden behind it. Scala has no such rule, because a trait is a type from the start. Scala also offers the other spelling — trait Container[Item], a type parameter rather than a member — and the choice is real: a parameter can vary per use site, a member is fixed by the implementation, which is exactly what associatedtype is.
Implicits & Type Classes
Given instances: arguments the compiler supplies
The feature with no Swift counterpart, and the one that explains the rest of the language.
struct Configuration { let verbose: Bool let prefix: String } // Swift has no implicit parameters. The context is threaded through by hand, // on every call, all the way down — or hidden in a global/singleton. func log(_ message: String, _ configuration: Configuration) { if configuration.verbose { print("\(configuration.prefix) \(message)") } } func process(_ items: [String], _ configuration: Configuration) { log("processing \(items.count)", configuration) items.forEach { log("item \($0)", configuration) } } process(["a", "b"], Configuration(verbose: true, prefix: "[app]"))
case class Configuration(verbose: Boolean, prefix: String) object Main { // A 'using' parameter: callers do not pass it, the compiler finds it. def log(message: String)(using configuration: Configuration): Unit = if (configuration.verbose) println(s"${configuration.prefix} $message") // 'process' takes one only to PASS IT ON, so it need not even name it. def process(items: List[String])(using Configuration): Unit = { log(s"processing ${items.size}") items.foreach(item => log(s"item $item")) } def main(args: Array[String]): Unit = { // 'given' supplies the value; every call below it that needs a // Configuration gets this one. given Configuration = Configuration(verbose = true, prefix = "[app]") process(List("a", "b")) // threaded all the way down, with nothing written } }
One given at the top, and every call below it that needs a Configuration gets one — including calls three layers down, which is where the hand-threading in the left column becomes genuinely painful (add a parameter to a leaf function and every caller changes). Notice that process writes (using Configuration) with no name at all: it never reads the value, it only has to be eligible to hand it on. This is how Scala passes an ExecutionContext to every Future, a transaction to every query, and a logger to everything. The cost is the one everybody warns you about — when no value is found, the error says one is missing but not why the candidate you expected was ineligible. The two keywords exist to make that answerable: older Scala spelled both halves implicit, and separating given for the supply from using for the demand is what lets the compiler say which half it could not satisfy.
Type classes: retroactive conformance, taken further
Swift can already do half of this — an extension can make an existing type conform to an existing protocol. Watch what the other half buys.
protocol JsonWritable { func toJson() -> String } // Retroactive conformance: Swift CAN do this, and it is one of its best features. extension Int: JsonWritable { func toJson() -> String { String(self) } } extension String: JsonWritable { func toJson() -> String { "\"\(self)\"" } } // Conditional conformance: an Array is writable IF its element is. extension Array: JsonWritable where Element: JsonWritable { func toJson() -> String { "[" + map { $0.toJson() }.joined(separator: ",") + "]" } } print(36.toJson()) print("Ada".toJson()) print([1, 2, 3].toJson()) print([[1, 2], [3]].toJson()) // nested, and it works
trait JsonWriter[A] { def write(value: A): String } object JsonWriter { // The instances live in the companion, which the compiler searches on its // own — so they are found automatically, with no import. given intWriter: JsonWriter[Int] = (value: Int) => value.toString given stringWriter: JsonWriter[String] = (value: String) => "\"" + value + "\"" // An instance that DEPENDS on another instance — the conditional conformance. given listWriter[A](using inner: JsonWriter[A]): JsonWriter[List[A]] = (values: List[A]) => values.map(inner.write).mkString("[", ",", "]") } object Main { def toJson[A](value: A)(using writer: JsonWriter[A]): String = writer.write(value) def main(args: Array[String]): Unit = { println(toJson(36)) println(toJson("Ada")) println(toJson(List(1, 2, 3))) // JsonWriter[List[Int]] — SYNTHESIZED println(toJson(List(List(1, 2), List(3)))) // and nested, assembled recursively // println(toJson(3.7)) ← compile error: no JsonWriter[Double] in scope } }
Both columns work, and this is the closest Swift gets to a type class — retroactive and conditional conformance is genuinely rare and genuinely good. The difference is where the behavior lives. Swift attaches it to the type (an Int is now, everywhere in the program, a JsonWritable), so there can be exactly one conformance and a conflict between two libraries is unresolvable. Scala keeps it in a separate value — the given JsonWriter[Int] — which the compiler assembles per call site — so you can have two different encodings in scope in different files, swap one for testing, or pass one explicitly. That indirection is why every Scala JSON, ordering, and equality library is built this way, and it is the price of admission for the higher-kinded abstractions in the next section.
extension, on Both Sides
Scala 3 has an extension keyword that reads almost exactly like Swift's, and the mechanism underneath is the implicit machinery rather than anything built in.
extension Int { var squared: Int { self * self } func times(_ body: (Int) -> Void) { for index in 0..<self { body(index) } } } print(3.squared) 2.times { index in print("run \(index)") } // An extension cannot add a stored property — only computed ones // and methods. It CAN add a protocol conformance, which is the one // thing Scala needs its implicit machinery for.
extension (number: Int) { def squared: Int = number * number def times(body: Int => Unit): Unit = (0 until number).foreach(body) } object Main { def main(args: Array[String]): Unit = { println(3.squared) 2.times(index => println(s"run $index")) } }
The one difference that matters is scope. A Swift extension on Int is visible to everything that imports the module — there is no way to add a method for one file only. A Scala extension is an ordinary definition you have to bring into scope, so two libraries can each add a squared to Int and nothing collides until a file imports both. That also makes the failure mode different: Swift tells you at the definition, Scala at the call site, with an ambiguity error naming both candidates.
Higher-Kinded Types
Abstracting over Array itself
Swift can abstract over a type. Scala can abstract over a type constructor — over List itself, before it has been given an element type — and this is what Swift cannot express at all.
// The signature we want: // // func double<F<_>>(_ container: F<Int>) -> F<Int> // // Swift cannot express F<_>: a generic parameter stands for a TYPE, never for a // type constructor. There is no way to write one function that works for both // [Int] and Int? — you write it once per container, forever. func doubleArray(_ container: [Int]) -> [Int] { container.map { $0 * 2 } } func doubleOptional(_ container: Int?) -> Int? { container.map { $0 * 2 } } print(doubleArray([1, 2, 3])) print(doubleOptional(21) ?? -1) print(doubleOptional(nil) ?? -1)
// F[_] is a type parameter that itself takes a type parameter. The signature is // literally "for any container F that can be mapped over". trait Mappable[F[_]] { def map[A, B](container: F[A])(transform: A => B): F[B] } object Mappable { given listMappable: Mappable[List] with { def map[A, B](container: List[A])(transform: A => B): List[B] = container.map(transform) } given optionMappable: Mappable[Option] with { def map[A, B](container: Option[A])(transform: A => B): Option[B] = container.map(transform) } } object Main { // ONE function. It works for List, for Option, and for anything else with an // instance — including types that do not exist yet. def double[F[_]](container: F[Int])(using mappable: Mappable[F]): F[Int] = mappable.map(container)(_ * 2) def main(args: Array[String]): Unit = { println(double(List(1, 2, 3))) println(double(Option(21))) println(double(Option.empty[Int])) } }
A Swift generic parameter always stands for a complete type, so F<_> is simply not writable and there is no way to say "any container that can be mapped over" — you write doubleArray, doubleOptional, doubleResult, and accept the duplication. (Swift's some Collection and its associated types get you partway for consuming a container, but never for returning the same shape.) In Scala, F[_] plus implicits is the foundation everything functional is built on: Functor, Monad and Traverse in Cats are exactly this trait with more laws, which is what lets one traverse work over every effect type in a program.
Variance Is Declared, Not Assumed
🚨 Swift decides for you: Array is covariant, and you cannot change it or ask for anything else. Scala makes you write a + or a - and then checks that you meant it.
class Animal { func speak() -> String { "..." } } class Dog: Animal { override func speak() -> String { "woof" } } // Swift's Array is covariant, built in, with no way to say so or to opt out. let dogs: [Dog] = [Dog()] let animals: [Animal] = dogs print(animals[0].speak()) // A user-defined generic is INVARIANT and cannot be made otherwise: struct Box<Element> { let value: Element } let dogBox = Box(value: Dog()) // let animalBox: Box<Animal> = dogBox — will not compile, ever. print(dogBox.value.speak())
class Animal { def speak: String = "..." } class Dog extends Animal { override def speak: String = "woof" } // +Element: a Box[Dog] IS a Box[Animal]. Legal only because Element // never appears in an argument position — the compiler checks. case class Box[+Element](value: Element) object Main { def main(args: Array[String]): Unit = { val dogs: List[Dog] = List(new Dog) val animals: List[Animal] = dogs // List is declared +A println(animals.head.speak) val dogBox = Box(new Dog) val animalBox: Box[Animal] = dogBox println(animalBox.value.speak) } }
The check is the point. Writing +Element on a box with a def put(item: Element) is rejected at the declaration, because that is the combination that lets you store a cat in a list of dogs — the hole Java left open in its arrays and pays for with a run-time ArrayStoreException. Scala's answer is to make you declare the intent and then prove it; Swift's is to hard-code the safe answer for the standard library and forbid the question everywhere else. -Element, contravariance, is the other direction and is what makes Function1[-In, +Out] substitutable the way intuition says it should be.
For-Comprehensions
One syntax for every fallible chain
A for … yield is not a loop — it is sugar for flatMap, so it works over anything with that method. Swift has no equivalent construct.
struct User { let name: String let managerId: Int? } let users: [Int: User] = [ 1: User(name: "Ada", managerId: nil), 2: User(name: "Grace", managerId: 1), ] func managerName(_ id: Int) -> String? { // Optional chaining gets you a long way — but each step is a separate hop, // and there is no way to abstract this shape over anything but optionals. guard let user = users[id], let managerId = user.managerId, let manager = users[managerId] else { return nil } return manager.name } print(managerName(2) ?? "(none)") print(managerName(1) ?? "(none)") print(managerName(99) ?? "(none)")
case class User(name: String, managerId: Option[Int]) object Main { val users = Map( 1 -> User("Ada", None), 2 -> User("Grace", Some(1)), ) // Each <- is a flatMap. Any None short-circuits the whole thing to None, and // the happy path reads top to bottom like ordinary code. def managerName(id: Int): Option[String] = for { user <- users.get(id) managerId <- user.managerId manager <- users.get(managerId) } yield manager.name // The SAME syntax over Either carries a typed error out of the chain. def parsePositive(text: String): Either[String, Int] = text.toIntOption.toRight("not a number").flatMap { number => if (number > 0) Right(number) else Left("not positive") } def pipeline(text: String): String = (for { parsed <- parsePositive(text) halved <- if (parsed % 2 == 0) Right(parsed / 2) else Left("not even") } yield s"ok: $halved").merge def main(args: Array[String]): Unit = { println(managerName(2)) println(managerName(1)) println(managerName(99)) println(pipeline("8")) println(pipeline("7")) println(pipeline("nope")) // And over List, the same syntax is a nested loop with a filter. println((for { number <- 1 to 3 if number % 2 == 1 letter <- List("a", "b") } yield s"$number$letter").toList) } }
Swift's guard let … , let … , let … else handles the optional case well, and that is where it stops: the shape cannot be reused for a chain of Results, a list comprehension, or a sequence of futures. Scala's for is sugar for flatMap, so the same three lines work over Option (short-circuit on None), Either (short-circuit on Left, carrying a typed reason out), List (a nested loop), Try, Future, or any type you give a flatMap to. That is the whole argument for the abstraction, and it is why fragmented per-container idioms are the concrete cost of not having it.
A for Is flatMap and map
The same for that chains fallible steps also does nested iteration, because it is not a loop — it is sugar for flatMap, map and withFilter.
let suits = ["♠", "♥"] let ranks = [1, 2, 3] // A nested loop building an array needs a mutable accumulator. var cards: [String] = [] for suit in suits { for rank in ranks where rank % 2 == 1 { cards.append("\(rank)\(suit)") } } print(cards) // Or flatMap, which is what Scala's for becomes: print(suits.flatMap { suit in ranks.filter { $0 % 2 == 1 }.map { "\($0)\(suit)" } })
object Main { def main(args: Array[String]): Unit = { val suits = List("♠", "♥") val ranks = List(1, 2, 3) // yield makes the for an EXPRESSION: no accumulator, no var. val cards = for { suit <- suits rank <- ranks if rank % 2 == 1 } yield s"$rank$suit" println(cards) // Which the compiler rewrites into exactly this: println(suits.flatMap(suit => ranks.withFilter(_ % 2 == 1).map(rank => s"$rank$suit"))) } }
The yield is the whole difference. Without it a Scala for is a statement run for its effects, the same as Swift's; with it the loop is an expression that builds a value, and the nesting turns into a flatMap chain the compiler writes for you. Because the rewriting is purely syntactic, the same shape works over Option, Either, Try and Future — anything with flatMap and map — which is why one construct covers both "iterate over two lists" and "do four things that might each fail".
Collections
Collections, and the underscore
The methods rhyme closely enough that you can guess most of them; the underscore is the piece with no Swift equivalent.
let names = ["Ada", "Grace", "Alan", "Barbara"] print(names.filter { $0.count > 3 }.map { $0.uppercased() }) print(names.map(\.count).reduce(0, +)) print(names.sorted()) print(names.first { $0.hasPrefix("G") } ?? "(none)") print(Dictionary(grouping: names, by: \.count).keys.sorted()) // Laziness is opt-in. print(Array(names.lazy.map { $0.count }.prefix(2))) var ages = ["Ada": 36] ages["Alan"] = 41 print(ages["Ada"] ?? 0)
object Main { def main(args: Array[String]): Unit = { val names = List("Ada", "Grace", "Alan", "Barbara") // '_' is the placeholder for a single-use lambda parameter — Swift's $0. println(names.filter(_.length > 3).map(_.toUpperCase)) println(names.map(_.length).sum) println(names.sorted) println(names.find(_.startsWith("G")).getOrElse("(none)")) println(names.groupBy(_.length).keys.toList.sorted) // collect fuses filter and map through a PARTIAL FUNCTION — no Swift analog. println(names.collect { case name if name.length == 3 => name.toUpperCase }) // .view is .lazy. println(names.view.map(_.length).take(2).toList) // The immutable Map is the default: updating returns a NEW map. val ages = Map("Ada" -> 36) println((ages + ("Alan" -> 41)).getOrElse("Ada", 0)) println(ages.get("Nobody").getOrElse(0)) // .get returns an Option } }
The operators line up (first(where:) is find, .lazy is .view), and _ is $0 — with the catch that each _ refers to a different parameter, so list.reduce(_ + _) means (a, b) => a + b and you cannot use _ twice for the same value. The genuinely new tool is collect, which takes a block of case clauses (a partial function) and keeps only the elements that match — a filter, a map, and a destructuring in one. Note also that Scala's List is persistent: adding an element returns a new list sharing its tail, which is how immutability stays cheap.
Grouping, Folding, and Sliding
Beyond map and filter, Scala's collection library is considerably wider than Swift's, and several of these have no standard-library equivalent at all.
let words = ["apple", "avocado", "banana", "blueberry", "cherry"] // Swift 5.9 got Dictionary(grouping:), which is groupBy. let byLetter = Dictionary(grouping: words, by: { $0.first! }) print(byLetter.keys.sorted()) print(words.reduce(0) { $0 + $1.count }) // No sliding, no partition-into-pairs: write it by hand. let pairs = zip(words, words.dropFirst()).map { "\($0)-\($1)" } print(pairs.count)
object Main { def main(args: Array[String]): Unit = { val words = List("apple", "avocado", "banana", "blueberry", "cherry") val byLetter = words.groupBy(_.head) println(byLetter.keys.toList.sorted) println(words.foldLeft(0)(_ + _.length)) // sliding, grouped, partition, span, sortBy, distinctBy, zipWithIndex — // all in the standard library. println(words.sliding(2).map(_.mkString("-")).size) } }
The names to carry over: reduce with a seed is foldLeft, Dictionary(grouping:) is groupBy, allSatisfy is forall, contains(where:) is exists, enumerated() is zipWithIndex, and joined(separator:) is mkString. The ones with no Swift counterpart are worth knowing about before you write them yourself: sliding and grouped for windows, span and partition for splitting on a predicate, groupMapReduce for the group-then-aggregate that otherwise takes three passes, and view for making the whole chain lazy.
Errors: throws vs Try/Either
throws becomes Try, or Either
Swift marks a failing function throws and unwinds. Scala returns the failure as a value, and the type says which of the three vocabularies it chose.
enum ParseError: Error { case notANumber(String) case notPositive(Int) } // 'throws' is in the signature, and 'try' is required at every call site. func parsePositive(_ text: String) throws -> Int { guard let number = Int(text) else { throw ParseError.notANumber(text) } guard number > 0 else { throw ParseError.notPositive(number) } return number } do { print(try parsePositive("42")) print(try parsePositive("-1")) } catch ParseError.notPositive(let number) { print("not positive: \(number)") } catch { print("failed: \(error)") } print((try? parsePositive("nope")) ?? -1)
import scala.util.{Try, Success, Failure} object Main { // Scala has exceptions (unchecked, invisible in the signature) — and would // rather you returned a VALUE. Try captures a throw; Either carries YOUR error. def parsePositive(text: String): Either[String, Int] = text.toIntOption match { case None => Left(s"not a number: $text") case Some(n) if n <= 0 => Left(s"not positive: $n") case Some(n) => Right(n) } def main(args: Array[String]): Unit = { parsePositive("42") match { case Right(number) => println(number) case Left(reason) => println(s"failed: $reason") } println(parsePositive("-1")) println(parsePositive("nope").getOrElse(-1)) // Try is 'try?' with the error kept: Success | Failure, and it pattern-matches. val parsed: Try[Int] = Try("nope".toInt) parsed match { case Success(value) => println(value) case Failure(error) => println(s"threw: ${error.getClass.getSimpleName}") } println(Try("21".toInt).map(_ * 2).getOrElse(-1)) // And try/catch exists, with 'catch' taking a block of CASES. val recovered = try { "nope".toInt } catch { case _: NumberFormatException => -1 } println(recovered) } }
Scala has exceptions and they are unchecked, so throws disappears from the signature — the information Swift puts in the type system is gone unless you put it back yourself, which is exactly what Either[Error, Value] does. The advantage over throws is that a failure is a value: it can be stored, mapped, collected into a list, and chained in a for-comprehension, none of which a thrown Swift error can do. Try is try? with the error preserved (and it pattern-matches, since Success/Failure are ordinary case classes), and catch takes a block of case clauses, so the full pattern language applies to exceptions too.
defer Becomes try-finally, or Using
Scala has no defer. Cleanup is try/finally, or Using, which closes the resource and hands you a Try rather than throwing.
func work() -> String { print("opened") defer { print("closed") } return "result" } print(work()) print("after")
import scala.util.{Try, Success, Failure} object Main { def work(): String = { println("opened") try "result" finally println("closed") // runs on every path out } def main(args: Array[String]): Unit = { println(work()) println("after") // Using does the same for anything with a close(), and reports // the outcome as a value instead of unwinding. val outcome: Try[Int] = Try("not a number".toInt) outcome match { case Success(number) => println(s"got $number") case Failure(error) => println(s"failed: ${error.getClass.getSimpleName}") } } }
The shapes differ in where the cleanup is written relative to what it protects. Swift's defer sits next to the acquisition, so the two lines are adjacent and the scope decides when it runs; Scala's finally sits at the far end of the block, which is worse for reading and better for one thing — try is an expression, so val answer = try … finally … is a value and there is no mutable variable to declare first. Using(resource) { … } is the form to reach for with a real file or socket: it closes in reverse order, suppresses a secondary failure into the primary one, and returns Try.
Futures & Memory
async/await becomes Future — and it is eager
A Swift Task and a Scala Future both stand for work in flight, and only one of them has already started.
import Foundation func fetch(_ id: Int) async -> String { try? await Task.sleep(for: .milliseconds(10)) return "user-\(id)" } // Structured concurrency: the group OWNS its children, waits for them, and // cancels them together. func fetchAll() async -> [String] { await withTaskGroup(of: String.self) { group in for id in 1...3 { group.addTask { await fetch(id) } } var results: [String] = [] for await result in group { results.append(result) } return results.sorted() } } print(await fetch(1)) print(await fetchAll())
import scala.concurrent.{Await, Future} import scala.concurrent.ExecutionContext.Implicits.global // an IMPLICIT parameter import scala.concurrent.duration._ object Main { // No 'async' keyword. A Future starts running the moment it is constructed, // and the thread pool arrives as an implicit ExecutionContext. def fetch(id: Int): Future[String] = Future { Thread.sleep(10) s"user-$id" } def main(args: Array[String]): Unit = { println(Await.result(fetch(1), 2.seconds)) // Already running by the time sequence sees them — this is awaitAll. val all = Future.sequence((1 to 3).map(fetch)) println(Await.result(all, 2.seconds).sorted.toList) // A for-comprehension over Futures is SEQUENTIAL unless you construct them // first — the classic Scala performance bug, and one Swift cannot have. val first = fetch(1) val second = fetch(2) val combined = for { a <- first b <- second } yield s"$a + $b" println(Await.result(combined, 2.seconds)) } }
Three assumptions break. A Future is eager: constructing it submits the work, so there is no async { } to write and no way to describe a computation without starting it. Nothing owns it — there is no task group, no structured concurrency, and no cancellation (you can ignore the result; the work runs on). And the thread pool arrives as an implicit ExecutionContext, which is why that odd import …Implicits.global appears in every tutorial. The compensation: a Future is an ordinary monad, so it composes with map, flatMap and the for-comprehension you already know — but read that last block carefully, because a for over futures constructed inside it runs them sequentially, which is the most common performance bug in Scala. On memory: the JVM has a tracing GC, so there is no ARC, no deinit, no weak, and cycles are collected for free.
Composing Futures With for
Once several things are in flight, Swift reaches for async let and a task group. Scala reaches for the same for comprehension it uses for Option, because a Future has flatMap too.
// Illustrative: a top-level await needs an async context. // // func fetch(_ id: Int) async -> Int { id * 10 } // // async let first = fetch(1) // starts now // async let second = fetch(2) // starts now, in parallel // let total = await first + second // // The two run concurrently because async let starts them eagerly, and // the task group is what generalizes it to a variable number. print("async let is Swift's parallel pair")
import scala.concurrent.{Future, Await} import scala.concurrent.duration.* import scala.concurrent.ExecutionContext.Implicits.global object Main { def fetch(id: Int): Future[Int] = Future { id * 10 } def main(args: Array[String]): Unit = { // 🚨 Both are already RUNNING — a Future starts on creation. val first = fetch(1) val second = fetch(2) val total = for { a <- first b <- second } yield a + b println(Await.result(total, 5.seconds)) } }
🚨 The trap is that the for reads sequentially and is not: the two fetch calls started before it, so it only joins them. Writing for a <- fetch(1); b <- fetch(2) instead runs them one after the other, because the second call does not happen until the first completes — the same source shape, half the speed, and nothing warns you. Swift makes the distinction visible in the syntax: async let starts eagerly, a bare await does not. The other half of the difference is that Future needs an ExecutionContext in scope and has no cancellation at all, where Swift's task tree carries both.

Thank you — anything else?