PONY λ M2 Modula-2

Swift.CodeCompared.To/Scala

An interactive executable cheatsheet comparing Swift and Scala

Swift 6.3 Scala 2.13
Basics & Syntax
Hello, World
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
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.
Optionals & Option
Optional<T> becomes Option[T] — a value, not syntax
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).
Value Semantics & Case Classes
struct becomes case class — with a caveat
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.
Enums & Sealed Traits
Enums with associated values become sealed traits
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))
sealed trait Shape case class Circle(radius: Double) extends Shape case class Rectangle(width: Double, height: Double) extends Shape case object Point extends Shape // no payload, so a singleton object Main { // 'sealed' means the compiler knows every subtype (they must be in this file), // 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)) } }
The structure maps directly — sealed trait plus a case class per case is Swift's enum with associated values, and match destructures exactly as switch does. One difference matters and it is a downgrade: an inexhaustive match on a sealed type is a warning in Scala, not an error, so it compiles and throws MatchError at run time. Every serious Scala build therefore turns on -Xfatal-warnings, which restores the guarantee you take for granted. (Scala 3 adds a real enum keyword that expresses this in one line.)
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.
Protocols & Traits
Protocols become traits — with state, and stackable
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.
Implicits & Type Classes
Implicit parameters: 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 { // An 'implicit' parameter list: callers do not pass it, the compiler finds it. def log(message: String)(implicit configuration: Configuration): Unit = if (configuration.verbose) println(s"${configuration.prefix} $message") // 'process' takes one only to PASS IT ON — and it does that invisibly. def process(items: List[String])(implicit configuration: Configuration): Unit = { log(s"processing ${items.size}") items.foreach(item => log(s"item $item")) } def main(args: Array[String]): Unit = { implicit val configuration: Configuration = Configuration(verbose = true, prefix = "[app]") process(List("a", "b")) // threaded all the way down, with nothing written } }
One implicit val 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). 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 the compiler cannot find an implicit, the error tells you what is missing but not why the candidate you expected was ineligible — and Scala 3 renamed the whole thing to given/using partly to make that failure legible.
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 is part of IMPLICIT SCOPE, so // they are found automatically, with no import. implicit val intWriter: JsonWriter[Int] = (value: Int) => value.toString implicit val stringWriter: JsonWriter[String] = (value: String) => "\"" + value + "\"" // An instance that DEPENDS on another instance — the conditional conformance. implicit def listWriter[A](implicit inner: JsonWriter[A]): JsonWriter[List[A]] = (values: List[A]) => values.map(inner.write).mkString("[", ",", "]") } object Main { def toJson[A](value: A)(implicit 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 JsonWriter[Int] instance), 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.
Higher-Kinded Types
Abstracting over Array itself
// 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 { implicit val listMappable: Mappable[List] = new Mappable[List] { def map[A, B](container: List[A])(transform: A => B): List[B] = container.map(transform) } implicit val optionMappable: Mappable[Option] = new Mappable[Option] { 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])(implicit 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.
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.
Collections
Collections, and the underscore
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.
Errors: throws vs Try/Either
throws becomes Try, or Either
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.
Futures & Memory
async/await becomes Future — and it is eager
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.