Skip to content

Scala Cheatsheet

Hybrid functional/OO language on the JVM.

01

Basics

Variables & Types

Prefer val (immutable) over var (mutable) for safer, more predictable code. Scala infers types, but explicit annotations aid readability for public APIs. Everything is an object—no primitives (Int, Boolean are classes).

scala
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List[Int] = List(1, 2, 3)
println(name.getClass)  // class java.lang.String

String Interpolation

s"..." enables ${expr} interpolation. f"..." adds printf-style formatting (%s, %.2f). raw"..." disables escape sequences. These prefixes make string building type-safe and readable.

scala
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg")  // formatted
println(raw"No \n escape")  // raw string

Tuples

Tuples group 2-22 heterogeneous values. Access via _1, _2 (1-indexed). Destructure with val (a, b) = tuple. For more than 2 elements, prefer case classes for named fields and better readability.

scala
val pair = ("Alice", 30)
println(pair._1)  // Alice
println(pair._2)  // 30
val (name, age) = pair  // destructure
println(s"${name}: ${age}")
// Scala 3: val p = ("a", 1, 2.0)
val triple = ("a", 1, 2.0)
println(triple._3)  // 2.0

Type Inference & Ascription

Scala infers types for local variables and return types. Use explicit types for public APIs, recursive functions, and ambiguous cases. Type ascription (expr: Type) forces a type—useful for upcasting or disambiguation.

scala
val x = 42           // Int inferred
val y: Long = 42      // explicit Long
val z = 42: Long      // type ascription
val list = List(1, 2, 3)  // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2  // return type inferred

Unit & Nothing

Unit is like void (one value: ()). Nothing is the bottom type with no instances—used for functions that never return (throw, infinite loop). Nothing is a subtype of all types, enabling flexible type inference.

scala
def printIt(x: Int): Unit = println(x)  // like void
val u: Unit = ()    // Unit has one value: ()
// Nothing is the bottom type—no instances
def error(msg: String): Nothing =
  throw new RuntimeException(msg)
// Nothing is a subtype of everything
val n: Nothing = error("boom")
02

Strings

Common String Methods

Scala strings are Java strings with extra methods via implicit conversions (StringOps). Most methods return new strings (immutable). Use these instead of manual loops for clarity and correctness.

scala
val s = "Hello, World"
println(s.length)        // 12
println(s.toUpperCase)   // HELLO, WORLD
println(s.toLowerCase)   // hello, world
println(s.split(", "))   // Array(Hello, World)
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reverse)       // dlroW ,olleH
println(s.contains("World"))  // true

Multiline Strings (Triple-Quote)

Triple-quoted strings preserve all whitespace and newlines. Use stripMargin with | to align code cleanly—only text after | is kept. Ideal for SQL, JSON, or templates embedded in code.

scala
val sql = """
  SELECT * FROM users
  WHERE age > 18
  ORDER BY name
"""
println(sql.trim)
// StripMargin for clean indentation
val text = """|Hello
              |World""".stripMargin
println(text)  // Hello
World

String Building

mkString joins collections with optional prefix/suffix—idiomatic and efficient. Use StringBuilder for building large strings in loops. Avoid repeated + concatenation in loops (creates many intermediate objects).

scala
val parts = List("apple", "banana", "cherry")
println(parts.mkString(", "))     // apple, banana, cherry
println(parts.mkString("[", ", ", "]"))  // [apple, banana, cherry]
val sb = new StringBuilder
for (p <- parts) sb.append(p).append(" ")
println(sb.toString.trim)

String to Number

toInt/toDouble throw on invalid input. Use toIntOption (Scala 2.13+) for safe parsing returning Option. For bulk parsing, use Try or Either to handle errors functionally without exceptions.

scala
val n = "42".toInt        // 42
val d = "3.14".toDouble    // 3.14
val b = "true".toBoolean   // true
val safe = "abc".toIntOption  // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result)  // 0

Regex

.r converts a string to a Regex. findFirstIn returns Option, findAllIn returns an iterator. Use in pattern matching with case email(e) => for extraction. Regex is Java's Pattern under the hood.

scala
import scala.util.matching.Regex
val email: Regex = "[\w.]+@[\w]+\.[a-z]+".r
val text = "Contact: [email protected]"
email.findFirstIn(text) match {
  case Some(e) => println(s"Found: ${e}")
  case None => println("No email")
}
val replaced = "[0-9]+".r.replaceAllIn("a1b2c3", "#")
println(replaced)  // a#b#c#
03

Data Structures

List & Seq

List is an immutable singly-linked list—O(1) head/prepend, O(n) random access. Use Vector for random access (O(1) effective). +: prepends, :+ appends. Prefer immutable collections for thread safety.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.head)      // 1
println(nums.tail)      // List(2,3,4,5)
println(nums.reverse)   // List(5,4,3,2,1)
println(nums.take(2))   // List(1, 2)
println(nums.drop(2))   // List(3, 4, 5)
println(nums.mkString)  // 12345
val combined = 0 +: nums :+ 6  // List(0,1,2,3,4,5,6)

Map

Map is immutable—operations return new Maps. Use get(key) for Option access, getOrElse for defaults. + adds/updates, - removes. For mutable maps, use scala.collection.mutable.Map. Keys must be Hashable.

scala
val ages = Map("Alice" -> 30, "Bob" -> 25)
println(ages("Alice"))            // 30 (throws if missing)
println(ages.getOrElse("Eve", 0)) // 0 (safe)
val updated = ages + ("Eve" -> 28)  // new Map
val removed = ages - "Bob"
ages.foreach { case (k, v) => println(s"${k}: ${v}") }
println(ages.keys)  // Set(Alice, Bob)

Set

Set is immutable with O(1) contains. union (|), intersect (&), diff (~) for set operations. + adds, - removes. Use for deduplication and membership testing. Mutable variant: scala.collection.mutable.Set.

scala
val a = Set(1, 2, 3)
val b = Set(3, 4, 5)
println(a union b)        // Set(1,2,3,4,5)
println(a intersect b)    // Set(3)
println(a diff b)         // Set(1,2)
println(a subsetOf(Set(1,2,3,4)))  // true
val added = a + 6  // Set(1,2,3,6)

Option (Null Safety)

Option replaces null—Some(value) or None. Use map/filter/flatMap for transformations, getOrElse for defaults. For-comprehensions work on Option. This eliminates NullPointerException by making absence explicit in the type.

scala
def findUser(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
val name = findUser(1)
println(name.getOrElse("Unknown"))  // Alice
println(name.map(_.toUpperCase))    // Some(ALICE)
println(name.filter(_.startsWith("A")))  // Some(Alice)
val result = for {
  n <- findUser(1)
  if n.startsWith("A")
} yield n.toUpperCase  // Some(ALICE)

Array & Vector

Array is a mutable Java array (fastest, but no functional updates). Vector is immutable with effectively O(1) random access and updates—preferred for immutable random-access collections. Use List for sequential, Vector for indexed.

scala
val arr = Array(1, 2, 3, 4)  // mutable, Java array
arr(0) = 10
println(arr(0))  // 10
val vec = Vector(1, 2, 3, 4)  // immutable, fast random access
println(vec(2))  // 3
val updated = vec.updated(0, 10)  // Vector(10,2,3,4)
// Vector: O(1) random access + immutable
// Array: mutable, Java interop, fastest
04

Control Flow

If / Else (Expression)

In Scala, if/else is an expression that returns a value. This eliminates the need for a ternary operator. Both branches must have compatible types. Use this for concise conditional assignment.

scala
val score = 85
val grade =
  if (score >= 90) "A"
  else if (score >= 80) "B"
  else if (score >= 70) "C"
  else "F"
println(grade)  // B
// if returns a value—no ternary operator needed

For Comprehension

for comprehensions iterate and can filter (if guards) and transform (yield). Without yield, it's a loop; with yield, it builds a collection. to includes the end, until excludes it. Equivalent to flatMap/map/filter chains.

scala
for (i <- 1 to 5) println(i)  // 1 2 3 4 5
for (i <- 1 until 5) print(i)  // 1 2 3 4
// With yield (generates collection)
val doubled = for (n <- List(1,2,3)) yield n * 2
println(doubled)  // List(2, 4, 6)
// With filter (guard)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(evens.toList)  // List(2,4,6,8,10)

Match (Pattern Matching)

match is Scala's powerful pattern matching—like switch on steroids. Supports literals, OR (|), guards (if), type matching, and destructuring. Must be exhaustive (compiler warns on missing cases). It's an expression returning a value.

scala
val n = 2
val label = n match {
  case 0 => "zero"
  case 1 | 2 | 3 => "small"
  case x if x < 10 => "medium"
  case _ => "large"
}
println(label)  // small
// Match on types
def describe(x: Any): String = x match {
  case i: Int => s"Int: ${i}"
  case s: String => s"String: ${s}"
  case _ => "unknown"
}

While & Do-While

while/do-while are imperative loops that return Unit. They require mutable state (var). Prefer for-comprehensions or recursion for idiomatic functional Scala. Use while only when performance demands it or for side effects.

scala
var i = 0
while (i < 3) {
  println(i)
  i += 1
}
var j = 0
do {
  println(j)
  j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutability

Try / Catch / Finally

try/catch/finally is like Java but uses pattern matching in catch. Prefer Try for functional error handling—it wraps exceptions as Success/Failure values, enabling map/flatMap chains without try/catch boilerplate.

scala
import scala.util.{Try, Success, Failure}
val result = try {
  "abc".toInt
} catch {
  case e: NumberFormatException => 0
} finally {
  println("cleanup")
}
println(result)  // 0
// Functional alternative
val r2 = Try("abc".toInt).getOrElse(0)
println(r2)  // 0
05

Functions

Method Definition

Methods use def name(params): ReturnType = body. Default params and named arguments supported. Unit return = side effect only. Single-expression bodies omit braces. = is required (without it, it's a procedure returning Unit).

scala
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
  s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg

Lambda (Anonymous Function)

Lambdas: (params) => body. Use _ as shorthand for single params (x => x * 2 becomes _ * 2). Multiple _'s refer to different params (_ + _). Lambdas are first-class—pass them to map, filter, reduce, etc.

scala
val square = (x: Int) => x * x
println(square(5))  // 25
val nums = List(1, 2, 3)
println(nums.map(_ * 2))      // List(2, 4, 6)
println(nums.filter(_ > 1))   // List(2, 3)
println(nums.reduce(_ + _))   // 6
// _ is shorthand for the parameter

Higher-Order Functions

Higher-order functions take or return functions. This enables powerful abstractions: map/filter/reduce, composition, partial application. makeAdder returns a closure capturing n. This is the heart of functional programming.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5))  // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10))  // 15
// Functions returning functions = currying-like

Currying & Partial Application

Currying splits params into multiple lists: def f(a)(b). Partially apply with _ to create specialized functions. Multiple param lists improve type inference (the compiler can infer f's types from the list). Common in collection APIs.

scala
def add(a: Int)(b: Int): Int = a + b  // curried
val add5 = add(5)_  // partially applied
println(add5(3))  // 8
// Multiple parameter lists
def foldLeft[A, B](list: List[A])(z: B)(f: (B, A) => B): B = ???
// Helps type inference
val sum = List(1,2,3).foldLeft(0)(_ + _)

By-Name & Lazy

By-name params (=> T) are evaluated lazily on each use—useful for logging (skip expensive msg if disabled) and custom control structures. lazy val defers initialization until first access—use for expensive or optional values.

scala
// By-name parameter: evaluated on each use
def debug(msg: => String): Unit =
  if (debugEnabled) println(msg)
// Lazy evaluation
lazy val expensive = computeHeavy()
println(expensive)  // computed now
def computeHeavy(): Int = { println("computing"); 42 }
06

Classes & OOP

Class & Constructor

Primary constructor params are in the class signature. val params become immutable fields (public getter), var mutable. The class body IS the constructor. Auxiliary constructors use def this(...) and must call another constructor.

scala
class Person(val name: String, val age: Int) {
  // Constructor params with val/var become fields
  def greet: String = s"Hi, I'm ${name}"
  def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet)   // Hi, I'm Alice
println(p.name)    // Alice (val field)
println(p.isAdult) // true

Case Class

Case classes are immutable data classes with auto-generated equals, hashCode, toString, copy, and companion object with apply/unapply. Use for data modeling and pattern matching. No 'new' needed. They're the foundation of ADTs in Scala.

scala
case class Point(x: Int, y: Int)
val p1 = Point(3, 4)  // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2)  // true (value equality)
val moved = p1.copy(x = 5)  // Point(5, 4)
println(p1.x, p1.y)  // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companion

Traits (Interfaces with Implementation)

Traits are like Java interfaces but can have implementation. A class can mix in multiple traits (with extends/with). Traits enable multiple inheritance of behavior. Use for shared interfaces, mixins, and stackable modifications via linearization.

scala
trait Greetable {
  def name: String  // abstract
  def greet: String = s"Hello, ${name}"  // concrete
}
trait Named {
  val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet)  // Hello, Alice
// Stackable traits via linearization

Object (Singleton)

object declares a singleton (one instance). Companion objects (same name as a class) hold static-like methods, factories (apply), and extractors (unapply). Use apply for factory methods so callers omit 'new'. This is idiomatic Scala.

scala
object Config {
  val version = "1.0"
  def load(): Map[String, String] = Map("key" -> "value")
}
println(Config.version)  // 1.0
// Companion object (same name as class)
class Person(val name: String)
object Person {
  def apply(name: String): Person = new Person(name)
}
val p = Person("Alice")  // uses apply, no 'new'

Inheritance & Abstract Class

abstract class can have unimplemented members. Use extends to inherit, override to redefine. Prefer traits for mixins (multiple inheritance). Use abstract class when you need constructor params or want a base type. Single inheritance of classes.

scala
abstract class Shape {
  def area: Double  // abstract
  def describe: String = s"Area: ${area}"
}
class Circle(r: Double) extends Shape {
  def area: Double = math.Pi * r * r
}
val c = new Circle(5)
println(c.describe)  // Area: 78.53...
// override required for concrete members
// abstract class vs trait: use abstract for base, trait for mixins
07

Collections & Functional

Map / Filter / Fold

map transforms, filter selects, reduce/foldLeft aggregate. These are the core of functional data transformation. foldLeft takes a seed and is associative; reduce requires non-empty. Use these instead of loops for clarity and immutability.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2))           // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0))   // List(2,4)
println(nums.reduce(_ + _))        // 15
println(nums.foldLeft(0)(_ + _))   // 15
println(nums.sum)                  // 15
println(nums.mkString(", "))       // 1, 2, 3, 4, 5

FlatMap & For-Comprehension

flatMap maps and flattens in one step—essential for nested collections and monadic operations. For-comprehensions are syntactic sugar for flatMap/map/filter chains. Use for complex nested transformations—it's more readable.

scala
val nested = List(List(1, 2), List(3, 4))
println(nested.flatten)        // List(1,2,3,4)
println(nested.flatMap(_.map(_ * 2)))  // List(2,4,6,8)
// Equivalent for-comprehension:
val result = for {
  inner <- nested
  n <- inner
} yield n * 2
println(result)  // List(2,4,6,8)

Grouping & Sorting

groupBy partitions by a key into a Map. sorted sorts naturally, sortBy by a key function, sortWith with a comparator. These return new collections (immutable). Use for data analysis, categorization, and ordering.

scala
val words = List("apple", "bat", "cat", "ant")
val byFirst = words.groupBy(_.head)
// Map(a -> List(apple, ant), b -> List(bat), c -> List(cat))
println(byFirst)
val sorted = words.sorted  // List(ant, apple, bat, cat)
val byLen = words.sortBy(_.length)  // List(ant, bat, cat, apple)
val desc = words.sortWith(_ > _)  // descending
println(sorted, byLen)

Either & Try

Try wraps exceptions as Success/Failure values. Either represents Left (error) or Right (success)—use for domain errors where you want to distinguish error types. Both support map/flatMap for functional error propagation.

scala
import scala.util.{Try, Success, Failure}
def parse(s: String): Try[Int] = Try(s.toInt)
parse("42") match {
  case Success(n) => println(s"OK: ${n}")
  case Failure(e) => println(s"Err: ${e.getMessage}")
}
// Either for domain errors
def divide(a: Int, b: Int): Either[String, Int] =
  if (b == 0) Left("div by zero") else Right(a / b)
println(divide(10, 2))  // Right(5)
println(divide(10, 0))  // Left(div by zero)

Lazy Collections (View & Lazy)

.view makes collections lazy—operations are deferred until forced (toList, sum, etc.). Avoids intermediate collections for better performance on large data. LazyList (formerly Stream) enables infinite sequences—elements computed on demand.

scala
val nums = (1 to 1000000).view
val result = nums
  .filter(_ % 2 == 0)
  .map(_ * 2)
  .take(5)
  .toList  // forces evaluation
println(result)  // List(4, 8, 12, 16, 20)
// view = lazy, no intermediate collections
// LazyList (Stream) for infinite sequences
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
08

Pattern Matching

Matching Case Classes

Sealed traits + case classes form Algebraic Data Types (ADTs). The compiler checks exhaustiveness—add a case and it warns where matches need updating. Pattern matching destructures case classes directly. This is idiomatic Scala modeling.

scala
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
  case Circle(r) => math.Pi * r * r
  case Square(s) => s * s
  case Rect(w, h) => w * h
}
println(area(Circle(5)))  // 78.53...

Guards & Conditions

Guards (if condition) add runtime checks to patterns. They make matching more expressive. Order matters—first match wins. Use guards for ranges, conditions, or complex logic that simple patterns can't express.

scala
val n = 15
val desc = n match {
  case x if x < 0 => "negative"
  case 0 => "zero"
  case x if x % 2 == 0 => "even"
  case _ => "odd"
}
println(desc)  // odd
// Guards add boolean conditions to patterns

Matching Collections

:: cons pattern destructures lists into head and tail. _* matches zero or more elements in arrays/lists. These patterns enable recursive list processing and structural decomposition. Powerful for parsing and tree traversal.

scala
val list = List(1, 2, 3, 4)
list match {
  case Nil => println("empty")
  case head :: Nil => println(s"one: ${head}")
  case head :: tail => println(s"head=${head}, rest=${tail}")
  case _ => println("other")
}
// head :: tail destructures a list
val arr = Array(1, 2, 3)
arr match {
  case Array(1, _*) => println("starts with 1")
  case _ => println("other")
}

Matching Option & Either

Pattern matching on Option/Either is idiomatic—Some(x)/None, Right(x)/Left(e). For-comprehensions desugar to flatMap with match. This makes error handling flow naturally without explicit if-else checks.

scala
def find(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
find(1) match {
  case Some(name) => println(s"Found: ${name}")
  case None => println("Not found")
}
// In for-comprehensions
val result = for {
  name <- find(1)
  upper = name.toUpperCase
} yield upper
println(result)  // Some(ALICE)

Extractors (unapply)

Custom extractors via unapply enable pattern matching on any type. The unapply method returns Option of the extracted values. This lets you define your own patterns—powerful for DSLs and parsing. Case classes auto-generate unapply.

scala
object Email {
  def unapply(s: String): Option[(String, String)] = {
    val parts = s.split("@")
    if (parts.length == 2) Some((parts(0), parts(1))) else None
  }
}
"[email protected]" match {
  case Email(user, domain) =>
    println(s"User: ${user}, Domain: ${domain}")
  case _ => println("Not an email")
}
09

Generics & Implicits

Generic Classes & Methods

Generics (type parameters [A]) write type-safe, reusable code. Use [A] for a single type, [A, B] for two. Type inference usually figures out the type. Generics are erased at runtime (JVM limitation) but checked at compile time.

scala
class Stack[A] {
  private var items: List[A] = Nil
  def push(x: A): Unit = { items = x :: items }
  def pop: Option[A] = items.headOption
}
val s = new Stack[Int]
s.push(1); s.push(2)
println(s.pop)  // Some(2)
def first[A](list: List[A]): Option[A] = list.headOption
println(first(List("a", "b")))  // Some(a)

Type Bounds

<: upper bound (A is a subtype), >: lower bound (A is a supertype). Context bounds (A: Ordering) require an implicit value of that type. View bounds (A <% B) are deprecated—use context bounds instead. These constrain type parameters.

scala
// Upper bound: A must be Animal or subclass
class Box[A <: Animal](val content: A)
// Lower bound: A must be Dog or superclass
class Kennel[A >: Dog](val occupant: A)
// Context bound: A must have an Ordering
def max[A: Ordering](a: A, b: A): A =
  if (implicitly[Ordering[A]].gt(a, b)) a else b
abstract class Animal { def name: String }
class Dog extends Animal { def name = "Rex" }

Implicit Parameters

Implicit parameters are injected by the compiler from scope. Use for configuration, type classes, or dependencies you don't want to pass everywhere. Declare with implicit val/def. The compiler searches the enclosing scope and companion objects.

scala
def greet(name: String)(implicit greeting: String): String =
  s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice"))  // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi"))  // Hi, Bob! (explicit override)
// Compiler finds implicit in scope

Type Classes (Implicit Conversions)

Type classes (via implicits) add behavior to types without modifying them—ad-hoc polymorphism. Define a trait, provide implicit instances, and use implicit params. This is how Ordering, Numeric, and Show work. More flexible than inheritance.

scala
trait Show[A] { def show(a: A): String }
object Show {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val strShow: Show[String] = identity
}
def printIt[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))
printIt(42)       // 42
printIt("hello")  // hello
// Type class: ad-hoc polymorphism

Extension Methods (Scala 2)

Implicit classes add extension methods to existing types. Define implicit class with a single param, and its methods become available on that type. Use to add utility methods to Int, String, etc. Scala 3 uses the cleaner 'extension' syntax.

scala
implicit class IntOps(val n: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to n).foreach(_ => f)
  def squared: Int = n * n
}
5.times { print("hi") }  // hihihihihi
println(5.squared)  // 25
// Scala 3: extension (n: Int) def squared = n * n
10

Concurrency & Future

Future & Async

Future represents an async computation. onComplete registers a callback. Never block (Await.result) in production web servers—it ties up threads. Use for-comprehensions to chain futures functionally. Requires an ExecutionContext.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
  Thread.sleep(1000)
  42
}
f.onComplete {
  case scala.util.Success(v) => println(s"Got ${v}")
  case scala.util.Failure(e) => println(s"Err: ${e}")
}
// Don't block in production—use callbacks or for-comprehensions

Composing Futures

For-comprehensions on Futures run them sequentially (each await the previous). For parallel execution, start all Futures first, then use Future.sequence to combine. Future.traverse maps + sequences in one step. This is the idiomatic way to compose async work.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f1 = Future { 10 }
val f2 = Future { 20 }
val sum = for {
  a <- f1
  b <- f2
} yield a + b  // Future(30)
// Parallel execution
val results = Future.sequence(List(
  Future { 1 }, Future { 2 }, Future { 3 }
))
results.map(_.sum)  // Future(6)

Parallel Collections

.par converts a collection to a parallel version—operations use multiple threads automatically. Good for CPU-bound work on large collections. Beware: non-associative operations (like subtraction) may give different results. Not for I/O-bound work.

scala
val nums = (1 to 1000000).toList
val sum = nums.par.sum  // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collections

Promise (Manual Future)

Promise is the writable side of a Future—you complete it manually with success/failure. Use when bridging callback-based APIs to Futures, or when you need to complete a Future from multiple places. Future is read-only; Promise is write-once.

scala
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println)  // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read side

Sync vs Async (Await)

Await.result blocks the current thread until the Future completes (with timeout). Use only in tests or main methods—blocking in async code defeats the purpose. In production, use callbacks (onComplete, map) or for-comprehensions to stay non-blocking.

scala
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result)  // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks instead
11

Implicits Deep Dive

Implicit Parameters

Implicit parameters are passed automatically by the compiler when an implicit value of the matching type is in scope. This reduces boilerplate for 'context' parameters (ExecutionContext, logging, configuration). The compiler searches: local scope, companion objects, implicit scope. You can always pass explicitly to override. Overuse makes code hard to trace—use for genuinely contextual dependencies.

scala
import scala.concurrent.ExecutionContext

// Method with implicit parameter
def process[A](data: List[A])
    (implicit ec: ExecutionContext): Future[Unit] = {
  Future { data.foreach(println) }
}

// The compiler finds an implicit ExecutionContext in scope
implicit val ec: ExecutionContext = ExecutionContext.global
process(List(1, 2, 3))  // ec passed automatically

// Explicitly providing (overrides implicit)
process(List(1, 2, 3))(myCustomEC)

// Multiple implicit parameters
def log(msg: String)(implicit
    level: Level, logger: Logger): Unit = {
  logger.log(level, msg)
}

Implicit Conversions

Implicit conversions automatically convert between types when needed. implicit class (extending AnyVal for zero overhead) adds extension methods to existing types—this is how Scala adds methods to Int, String, etc. Be careful: implicit conversions can make code confusing (what's being converted?). Prefer implicit classes for extensions over raw implicit defs. Enable with import scala.language.implicitConversions.

scala
import scala.language.implicitConversions

// Implicit conversion: one type to another
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // intToString(42) called implicitly

// Extension via implicit class (Scala 2.10+)
implicit class RichInt(val self: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
  def squared: Int = self * self
}

5.times { println("hi") }  // prints hi 5 times
3.squared  // 9

// Implicit conversion for type compatibility
implicit def javaToScalaList(jl: java.util.List[Int]): List[Int] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList  // converted

Implicit Resolution Priority

The compiler resolves implicits by priority: local scope > companion objects > imported > inherited. If two implicits of the same type are equally in scope, you get an 'ambiguous implicit' error. The LowPriorityImplicits trait pattern provides defaults that can be overridden by more specific implicits. Understanding resolution order is crucial for library design—place defaults in low-priority traits so users can override.

scala
// Priority of implicit resolution (highest to lowest):
// 1. Local implicit (defined in current scope)
implicit val ec1: ExecutionContext = ec1

// 2. Implicit in companion object
object MyService {
  implicit val ec2: ExecutionContext = ec2  // lower priority
}

// 3. Implicit scope (imported)
import somePackage.Implicits._

// 4. Implicit parameter default (inherited trait)
trait DefaultEc {
  implicit val ec: ExecutionContext = ExecutionContext.global
}

// More specific type wins
implicit def ord1: Ordering[Int] = ???
implicit def ord2: Ordering[Int] = ???  // ambiguous error!

// LowPriorityImplicits trait pattern
object MyLib {
  implicit val high: Ordering[Int] = ???
}
object MyLib extends LowPriorityImplicits
trait LowPriorityImplicits {
  implicit val low: Ordering[Int] = ???  // fallback
}

Context Bounds and Evidence

Context bounds [A: TypeClass] are syntactic sugar for implicit parameters—they assert that an implicit TypeClass[A] exists. Use implicitly[TypeClass[A]] (Scala 2) or summon[TypeClass[A]] (Scala 3) to retrieve it. Context bounds make type class constraints readable: def sort[A: Ordering]. Multiple bounds stack: [A: Ordering: Numeric]. This is the idiomatic way to express type class requirements.

scala
// Context bound: [A: Ordering] means there's an implicit Ordering[A]
def max[A: Ordering](a: A, b: A): A = {
  val ord = implicitly[Ordering[A]]  // retrieve the implicit
  if (ord.gt(a, b)) a else b
}

// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
  if (ord.gt(a, b)) a else b

// summon (Scala 3) instead of implicitly
def max3[A: Ordering](a: A, b: A): A = {
  val ord = summon[Ordering[A]]
  if (ord.gt(a, b)) a else b
}

// Type class evidence
def sort[A: Ordering](list: List[A]): List[A] =
  list.sorted  // uses the implicit Ordering

// Multiple context bounds
def process[A: Ordering: Numeric](x: A, y: A): A = ???

Implicit Scope and Companion Objects

Implicit scope is broader than just the current scope—it includes companion objects of the types involved. This is why you don't need to import Ordering[Int]: it lives in Int's companion. This mechanism makes type classes ergonomic: define the instance in the type's companion, and it's automatically available. Package objects hold shared implicits for a package. This design enables 'zero-import' type class usage.

scala
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
  implicit val ordering: Ordering[UserId] =
    Ordering.by(_.value)
}

// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion

// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters

// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }

// Package object for shared implicits
package object myapp {
  implicit val ec: ExecutionContext = ExecutionContext.global
  type Id = Long
}
12

Type Classes

Defining a Type Class

A type class is a trait parameterized by type, with instances providing behavior for specific types. Unlike inheritance, you can add type class instances retroactively (for types you don't own). Show[A] defines how to display A. Instances live in the companion object (automatic implicit scope). This is ad-hoc polymorphism—different behavior per type without modifying the types. Type classes are Scala's most powerful abstraction.

scala
// Type class: a trait parameterized by type
trait Show[A] {
  def show(a: A): String
}

// Instances for specific types
object Show {
  // Instance for Int
  implicit val showInt: Show[Int] = (a: Int) => a.toString

  // Instance for String
  implicit val showString: Show[String] = (s: String) => s""$s""

  // Instance for List (recursive)
  implicit def showList[A](implicit s: Show[A]): Show[List[A]] =
    (list: List[A]) => list.map(s.show).mkString("[", ", ", "]")
}

// Usage with implicit parameter
def print[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))

print(42)           // 42
print("hello")      // "hello"
print(List(1, 2, 3))  // [1, 2, 3]

Using Type Classes (syntax sugar)

Context bounds [A: Show] + summon retrieve type class instances. Extension methods (implicit class) add methods like .show that use the type class. This combination gives a clean API: 42.show works if Show[Int] exists. The standard library provides many type classes: Numeric, Ordering, Eq, Monoid (Cats). Importing syntax (Numeric.Implicits._) adds operators like + and sum that use the type class.

scala
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
  items.foreach(a => println(summon[Show[A]].show(a)))

// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
  def show(implicit s: Show[A]): String = s.show(a)
}

42.show           // "42"
"hi".show         // ""hi""
List(1,2).show    // "[1, 2]"

// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show

// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
  summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
    summon[Numeric[A]].zero)(summon[Numeric[A]].plus))

// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sum

Common Type Classes (Cats/Scalaz)

Cats and Scalaz provide standard type classes. Monoid (empty + combine) enables generic aggregation. Functor (map) and Monad (pure + flatMap) abstract over containers (List, Option, Future, IO). Eq provides type-safe equality (no accidental cross-type comparisons). These compose: a Monad is a Functor, a Monoid is a Semigroup. Type classes enable writing generic, reusable code that works across many types.

scala
// Monoid: combine values with empty
trait Monoid[A] {
  def empty: A
  def combine(a: A, b: A): A
}
object Monoid {
  implicit val intAdd: Monoid[Int] = new Monoid[Int] {
    def empty = 0
    def combine(a: Int, b: Int) = a + b
  }
  implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
    def empty = Nil
    def combine(a: List[A], b: List[A]) = a ++ b
  }
}

// Functor: map over structure
trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

// Monad: chain operations
trait Monad[F[_]] {
  def pure[A](a: A): F[A]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}

// Eq: type-safe equality
trait Eq[A] {
  def eqv(a: A, b: A): Boolean
}

// Semigroup: combine (no empty)
trait Semigroup[A] {
  def combine(a: A, b: A): A
}

Laws and Testing Type Classes

Type class laws are mathematical properties instances must satisfy. Monoid requires associativity and identity. Functor requires identity and composition preservation. Libraries like Cats provide law definitions; discipline + ScalaCheck auto-tests them. Laws are why type classes are powerful: generic code (like foldMap) works correctly for any law-abiding instance. Always verify your instances satisfy the laws—bugs in instances break all generic code using them.

scala
// Type class laws: properties that must hold
// Monoid laws:
// 1. Left identity:  combine(empty, a) == a
// 2. Right identity: combine(a, empty) == a
// 3. Associativity:  combine(a, combine(b, c)) == combine(combine(a, b), c)

// Functor laws:
// 1. Identity:    map(fa)(identity) == fa
// 2. Composition: map(fa)(f andThen g) == map(map(fa)(f))(g)

// Testing laws with ScalaCheck (discipline)
import org.scalacheck.Prop.forAll
import cats.kernel.laws.MonoidLaws

class MonoidSpec extends munit.FunSuite with Discipline {
  checkAll("Int Monoid", MonoidLaws[Int].monoid)
}

// Custom law test
def monoidLeftIdentity[A](implicit m: Monoid[A], arb: Arbitrary[A]) =
  forAll { (a: A) =>
    m.combine(m.empty, a) == a
  }

// Laws make type classes trustworthy:
// if an instance satisfies laws, generic code works correctly

Type Class Derivation (Scala 3)

Scala 3 simplifies type class derivation with 'derives' keyword and Mirror. The compiler can auto-generate instances for case classes and enums by composing element instances. This eliminates the boilerplate of writing instances for every case class (common in Scala 2 with shapeless). Libraries like Cats and Circe support Scala 3 derivation. The Mirror type gives compile-time access to a type's structure (field types, labels) for generic programming.

scala
// Scala 3: derive type class instances automatically
import scala.deriving.Mirror

trait Show[A] {
  def show(a: A): String
}

object Show {
  // Inline given for derivation
  given showInt: Show[Int] with
    def show(a: Int) = a.toString

  given showString: Show[String] with
    def show(s: String) = s""$s""

  // Derive for products (case classes)
  given showProduct[A](using m: Mirror.ProductOf[A])
      (using ev: Show[m.MirroredElemTypes]): Show[A] with
    def show(a: A): String = ???

  // Or use Scala 3's derivation
  inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}

// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automatically
13

For Comprehensions Deep Dive

Basic For Comprehensions

For comprehensions are syntactic sugar for flatMap/map/withFilter. Each <- is flatMap (except the last, which is map). if guards become withFilter. yield makes it return a collection; omitting yield makes it imperative (foreach). This works on any type with flatMap/map (Monad): List, Option, Future, Try, IO. Mastering for-comprehensions is key to idiomatic Scala—they replace nested maps/flatMaps with readable sequential syntax.

scala
// For comprehension: syntactic sugar for flatMap/map
val result = for {
  x <- List(1, 2, 3)
  y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)

// Desugared:
List(1, 2, 3).flatMap { x =>
  List(10, 20).map { y => x + y }
}

// With filters (if guards)
val evens = for {
  x <- 1 to 10
  if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)

// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)

// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x)  // 1, 2, 3

For with Option and Future

For-comprehensions work on any Monad. With Option, they short-circuit on None (return None). With Future, they short-circuit on failure. This makes sequential async/may-fail code read like straight-line imperative code while remaining functional. Each <- line can depend on previous bindings. This is far cleaner than nested flatMap calls. The same syntax works for Try, Either, IO, and custom monads.

scala
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???

val email: Option[String] = for {
  user <- getUser(42)
  email <- getEmail(user)
} yield email

// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))

// Future: chain async operations
val result: Future[Int] = for {
  user <- fetchUser(1)      // Future[User]
  posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size

// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic composition

For with Either and Error Handling

Either is Scala's typed error handling. For-comprehensions chain Eithers, short-circuiting on Left (error). This is functional error handling—no exceptions, errors are values. The Left type is the error (usually String or a sealed trait). Either is right-biased in Scala 2.12+ (map/flatMap operate on Right). This pattern replaces try/catch with composable, type-safe error propagation. Cats' Validated is an alternative for accumulating errors.

scala
// Either for error handling
def parseAge(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"not a number: $s")

def validateAge(age: Int): Either[String, Int] =
  if (age >= 0) Right(age) else Left(s"negative: $age")

val result: Either[String, Int] = for {
  age <- parseAge("30")     // Right(30)
  valid <- validateAge(age) // Right(30)
} yield valid

// With error in chain
val error: Either[String, Int] = for {
  age <- parseAge("abc")    // Left("not a number: abc")
  valid <- validateAge(age) // skipped!
} yield valid
// error == Left("not a number: abc")

// Scala 3: for-comprehensions work with Either directly
// (Scala 2 needed either.map(_.right) or withFilter)

Desugaring and Custom Monads

Any type with flatMap and map supports for-comprehensions—this is the Monad pattern. Define these methods on your type to enable for-syntax. The = (not <-) creates a local binding (not desugared to flatMap). Understanding desugaring helps debug complex comprehensions and implement custom monads. The compiler translates for-comprehensions to flatMap/map/withFilter chains. This is why for works uniformly across List, Option, Future, IO, etc.

scala
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions

case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
  def flatMap[B](f: A => Box[B]): Box[B] = f(value)
  def withFilter(p: A => Boolean): Box[A] =
    if (p(value)) this else throw new NoSuchElementException
}

val result = for {
  x <- Box(10)
  y <- Box(20)
  if x < y
} yield x + y
// Box(30)

// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2.   Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
//   Box(20).withFilter(y => x < y).map(y => x + y))

// Assignment within for (= instead of <-)
for {
  x <- List(1, 2, 3)
  doubled = x * 2  // local val
} yield doubled

For vs Map/FlatMap (when to use)

Use for-comprehensions for 2+ dependent operations—they're more readable than nested flatMaps. For a single transformation, map is clearer. For flattening, flatMap directly. For side effects (no result), use for without yield. For-comprehensions shine when each step depends on the previous (monadic chaining). They make async/error-handling code read sequentially. Avoid deeply nested fors (>5 levels)—extract helpers for readability.

scala
// For: sequential, dependent operations
val result = for {
  user <- fetchUser(id)
  profile <- fetchProfile(user.id)
  avatar <- fetchAvatar(profile.avatarId)
} yield avatar

// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
  fetchProfile(user.id).flatMap(profile =>
    fetchAvatar(profile.avatarId)))

// Map: single transformation (no chaining)
users.map(_.name)  // simple, use map

// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts)  // List[Post]

// For without yield: side effects
for (user <- users) {
  saveToDb(user)
  sendEmail(user)
}

// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yield
14

Scala 3: Given & Using

Given Instances (replacing implicit val)

Scala 3 replaces implicit val/def with 'given'. givens are clearer and more explicit. Anonymous givens (given Type = ...) have compiler-generated names. 'given T with' defines instances with multiple methods. Conditional givens (given [A: Ordering]: Ordering[List[A]]) replace implicit defs. givens in companion objects are in implicit scope automatically, just like Scala 2. The keyword change reduces 'implicit' overload (which meant 4 things in Scala 2).

scala
// Scala 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global

// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global

// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global

// Given with 'with' for complex types
given Show[Int] with
  def show(a: Int): String = a.toString

// Given in companion object (automatic scope)
object MyType:
  given Ordering[MyType] = Ordering.by(_.id)

// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
  def compare(a: List[A], b: List[A]): Int =
    a.zip(b).find((x, y) => x != y) match
      case Some((x, y)) => summon[Ordering[A]].compare(x, y)
      case None => a.length - b.length

Using Clauses (replacing implicit params)

Scala 3 replaces implicit parameters with 'using'. This separates the two meanings of 'implicit' (parameters vs conversions). using clauses are passed explicitly with 'using' keyword. Context bounds [A: T] remain the same. summon[T] replaces implicitly[T] (clearer name). The using keyword makes it obvious at call sites when you're providing a context. This is a purely syntactic change—semantics are the same as implicit parameters.

scala
// Scala 2: implicit parameter
// def process[A](data: List[A])(implicit ec: ExecutionContext): Unit

// Scala 3: using
def process[A](data: List[A])(using ec: ExecutionContext): Unit =
  data.foreach(println)

// Context bound (unchanged)
def sort[A: Ordering](list: List[A]): List[A] = list.sorted

// Multiple using clauses
def log(msg: String)(using level: Level, logger: Logger): Unit =
  logger.log(level, msg)

// summon replaces implicitly
def max[A: Ordering](a: A, b: A): A =
  val ord = summon[Ordering[A]]
  if ord.gt(a, b) then a else b

// Provide explicitly with 'using'
process(data)(using myEC)
log("hello")(using Level.INFO, myLogger)

Extension Methods (Scala 3)

Scala 3 replaces implicit classes with 'extension'—clearer and more focused. extension (self: T) defines methods on T. Extensions can be generic (extension [A]). Combined with using, they provide type class syntax (42.show). Extensions are just method additions—they don't create wrapper objects (zero overhead with AnyVal in Scala 2, native in Scala 3). This is the idiomatic way to add methods to existing types in Scala 3.

scala
// Scala 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
//   def squared: Int = self * self
// }

// Scala 3: extension
extension (self: Int)
  def squared: Int = self * self
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)

5.squared   // 25
3.times { println("hi") }

// Extension on generic type
extension [A](self: List[A])
  def takeWhileInclusive(p: A => Boolean): List[A] = ???

// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)

42.show  // uses given Show[Int]

// Multiple extensions in one block
extension (s: String)
  def isBlank: Boolean = s.trim.isEmpty
  def words: List[String] = s.split(" ").toList

Enums and ADTs (Scala 3)

Scala 3 enums replace sealed trait + case objects for ADTs. They're more concise and support parameters, fields, and methods. Enum cases can have parameters (like case classes). Pattern matching is exhaustive-checked. Enums can be generic (Option[A]). This unifies enums and ADTs into one construct. For open hierarchies (extensible), use sealed trait + case classes still. For closed enumerations/ADTs, enum is cleaner.

scala
// Scala 3 enum (replaces sealed trait + case objects)
enum Color:
  case Red, Green, Blue

enum HttpStatus(val code: Int):
  case Ok extends HttpStatus(200)
  case NotFound extends HttpStatus(404)
  case Error extends HttpStatus(500)

// Pattern matching (exhaustive)
def describe(c: Color): String = c match
  case Color.Red => "red"
  case Color.Green => "green"
  case Color.Blue => "blue"

// Access fields
HttpStatus.Ok.code  // 200

// Parameterized enum cases
enum Option[+A]:
  case Some(value: A)
  case None

// ADT with methods
enum Tree[+A]:
  case Leaf(value: A)
  case Node(left: Tree[A], right: Tree[A])

  def size: Int = this match
    case Leaf(_) => 1
    case Node(l, r) => l.size + r.size + 1

Top-level Definitions and Indentation

Scala 3 allows top-level definitions—no need to wrap everything in an object. This simplifies file structure (like Python/Go). The new syntax supports both braces and significant indentation (optional). if/then replaces if/else with parens. match can be used as an expression without braces. These changes make Scala 3 more approachable while keeping backward compatibility. You can mix styles—use braces where they add clarity, indentation where it reduces noise.

scala
// Scala 3: top-level definitions (no class wrapper needed)
// File: MyMath.scala
def add(a: Int, b: Int): Int = a + b  // top-level function

val Pi: Double = 3.14159  // top-level val

type Id = Long  // top-level type alias

given Show[Int] = (a: Int) => a.toString  // top-level given

extension (i: Int) def squared: Int = i * i  // top-level extension

// Indentation-based syntax (optional, braces still work)
def factorial(n: Int): Int =
  if n <= 1 then 1
  else n * factorial(n - 1)

// Or with braces:
def factorial2(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial2(n - 1)
}

// if/then, match/case without braces
val sign = if x > 0 then 1 else -1
val desc = x match
  case 0 => "zero"
  case _ => "nonzero"
15

Extension Methods & Syntax

Implicit Classes (Scala 2)

implicit class (Scala 2.10+) adds extension methods to existing types. Extending AnyVal makes it zero-allocation (the compiler erases the wrapper). The class must take a single constructor parameter (the type being extended). Methods on the implicit class become available on the extended type. This is how Scala enriches Int, String, etc. Put implicit classes in a package object or utility object for sharing. In Scala 3, use 'extension' instead.

scala
// Scala 2: implicit class for extension methods
import scala.language.implicitConversions

implicit class RichString(val s: String) extends AnyVal {
  def wordCount: Int = s.split("\\s+").length
  def slug: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-")
  def encrypt(key: Int): String = s.map(c => (c + key).toChar)
}

"Hello World".wordCount  // 2
"My Blog Post!".slug     // "my-blog-post-"
"abc".encrypt(1)         // "bcd"

// AnyVal avoids allocation (zero overhead)
// implicit class must be in a trait, class, or object

// For generic extensions
implicit class RichList[A](val list: List[A]) extends AnyVal {
  def middle: Option[A] = list.lift(list.length / 2)
}

List(1, 2, 3, 4, 5).middle  // Some(3)

Extension Methods in Practice

Extension methods are the standard way to add utilities to collections and other types. distinctBy, chunked, tap are common additions. The === and =!= operators (via Eq type class) provide type-safe equality (unlike == which allows cross-type). Group extensions in an object and import where needed. This keeps core types clean while allowing domain-specific methods. Scala's collections library itself uses this pattern extensively.

scala
// Common pattern: add utility methods to collections
object CollectionExtensions {
  implicit class RichSeq[A](val seq: Seq[A]) extends AnyVal {
    def chunked(size: Int): Seq[Seq[A]] =
      seq.grouped(size).toSeq

    def distinctBy[B](f: A => B): Seq[A] =
      seq.groupBy(f).values.map(_.head).toSeq

    def tap(f: A => Unit): Seq[A] = {
      seq.foreach(f)
      seq
    }
  }
}

import CollectionExtensions._

List(1, 2, 3, 4, 5).chunked(2)  // List(List(1,2), List(3,4), List(5))
List("aa", "bb", "ab").distinctBy(_.head)  // List("aa", "ab")
List(1, 2, 3).tap(println)  // prints 1,2,3, returns List(1,2,3)

// Type class syntax via extension
implicit class EqOps[A](val a: A) extends AnyVal {
  def ===(b: A)(implicit eq: Eq[A]): Boolean = eq.eqv(a, b)
  def =!=(b: A)(implicit eq: Eq[A]): Boolean = !eq.eqv(a, b)
}

Implicit Conversions (Use Carefully)

Implicit conversions automatically convert types, but they're dangerous—code behavior becomes non-obvious. Scala 2.10+ requires explicit opt-in (scala.language.implicitConversions). Prefer extension methods (which add methods without changing types) over conversions (which change types). Legitimate uses: Java interop (converting between collection types), DSL construction. The compiler warns about implicit conversions—address these warnings seriously. In Scala 3, given Conversion[T, U] is the explicit mechanism.

scala
import scala.language.implicitConversions

// Implicit conversion between types
implicit def stringToInt(s: String): Int = s.toInt
val n: Int = "42"  // stringToInt("42")

// Dangerous: can cause surprising behavior
implicit def intToBoolean(n: Int): Boolean = n != 0
if (1) println("yes")  // works! (intToBoolean(1))

// Safer: use extension methods instead
extension (n: Int) def toBool: Boolean = n != 0
if (1.toBool) println("yes")

// When implicit conversions are appropriate:
// 1. Java interop (java.util.List <-> scala.List)
implicit def javaListToScala[A](jl: java.util.List[A]): List[A] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

// 2. Backward compatibility layers
// 3. DSL construction (use sparingly)

// Enable per-file: import scala.language.implicitConversions

Type-Level Programming

Type-level programming encodes information in types that the compiler checks. Phantom types (unused type parameters) track state (Open/Closed) preventing misuse (can't read a closed file). Peano naturals represent numbers as types. This enables compile-time correctness guarantees—bugs become compile errors. Used in state machines, units of measure, sized vectors. Powerful but complex—use when the safety is worth the type complexity. Libraries like shapeless (Scala 2) enable advanced type-level programming.

scala
// Phantom types: track state at type level
sealed trait State
trait Open extends State
trait Closed extends State

class File[S <: State] private (val path: String)

object File {
  def open(path: String): File[Open] = new File[Open](path)
}

def close[S <: Open](f: File[S]): File[Closed] =
  new File[Closed](f.path)  // unsafe cast internally

def read[S <: Open](f: File[S]): String = "data"
// def read[S <: Closed](f: File[S]): String  // compile error

val f = File.open("test.txt")
read(f)  // OK: f is File[Open]
val closed = close(f)
// read(closed)  // COMPILE ERROR: closed is File[Closed]

// Type-level natural numbers (Peano)
sealed trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat

// Compile-time list length check
type _0 = Zero
type _1 = Succ[_0]
type _2 = Succ[_1]

Opaque Types (Scala 3)

Opaque types (Scala 3) create zero-overhead newtypes—unlike value classes (AnyVal), they never box and are truly just the underlying type at runtime. The type distinction exists only at compile time, preventing mix-ups (Celsius vs Fahrenheit, UserId vs Long). Inside the defining object, the type and its underlying type are interchangeable; outside, they're distinct. This is the 'newtype' pattern from Haskell—domain-driven types without runtime cost. Use for IDs, units, and domain primitives.

scala
// Scala 2: value classes for zero-overhead wrappers
// case class UserId(value: Long) extends AnyVal

// Scala 3: opaque types (true zero overhead, no boxing)
object Types:
  type UserId = Long
  object UserId:
    def apply(value: Long): UserId = value

  type Email = String
  object Email:
    def apply(s: String): Email =
      require(s.contains("@"), "invalid email")
      s

import Types.*
val id: UserId = UserId(42)  // just a Long at runtime
val email: Email = Email("[email protected]")

// UserId and Long are NOT interchangeable outside the object
// def wrong(x: Long): UserId = x  // ERROR
// But inside the object, they're the same

// Newtype pattern: domain types without overhead
type Celsius = Double
type Fahrenheit = Double
// Prevents mixing up Celsius and Fahrenheit
def toF(c: Celsius): Fahrenheit = c * 9 / 5 + 32
16

Testing (ScalaTest & ScalaCheck)

ScalaTest Styles

ScalaTest offers multiple styles. FunSuite is simplest (test("name") { ... }). FlatSpec is BDD-style ("A Stack" should "..."). Matchers provides readable assertions (shouldBe, should contain, should throw). Choose one style per project for consistency. FunSuite is popular for unit tests; FlatSpec for behavior-focused tests. All styles support the same matchers and lifecycle hooks. The style affects syntax, not capabilities.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

// FunSuite: simple test functions
class MyTest extends AnyFunSuite with Matchers {
  test("addition works") {
    1 + 1 should be (2)
    1 + 1 shouldBe 2
    List(1, 2, 3) should contain (2)
  }

  test("string operations") {
    "hello".length shouldBe 5
    "hello" should startWith ("he")
  }
}

// FlatSpec: BDD-style
import org.scalatest.flatspec.AnyFlatSpec
class StackSpec extends AnyFlatSpec with Matchers {
  "A Stack" should "pop values in LIFO order" in {
    val stack = Stack(1, 2, 3)
    stack.pop() shouldBe 3
  }

  it should "throw on empty pop" in {
    val stack = Stack()
    a [NoSuchElementException] shouldBe thrownBy(stack.pop())
  }
}

// WordSpec, FreeSpec, PropSpec also available

Assertions and Matchers

Matchers provide expressive assertions. shouldBe/should be for equality. contain, have size, have key for collections. startWith/endWith/include for strings. thrownBy for exceptions. Custom matchers (be >, be <=) for comparisons. The 'should' DSL reads like English, making tests self-documenting. For complex assertions, use custom matchers or plain assert(condition). Avoid over-chaining matchers—readability over cleverness.

scala
import org.scalatest.matchers.should.Matchers._

// Equality
result shouldBe 42
result should be (42)
result should equal (42)

// Collections
list should contain (5)
list should not contain (0)
list shouldBe empty
list should have size 5
list should contain inOrder (1, 2, 3)
map should contain key ("name")
map should contain value ("Alice")

// Strings
s should startWith ("Hello")
s should endWith ("world")
s should include ("lo")
s should fullyMatch regex "H.*d".r

// Exceptions
a [IOException] should be thrownBy riskyOp()
the [IOException] thrownBy riskyOp() should have message "fail"

// Custom matchers
result should be > 0
result should be <= 100

// Type checks
result shouldBe a [List[_]]
result should be an [IllegalArgumentException]

BeforeAndAfter and Fixtures

BeforeAndAfterEach runs setup/teardown around each test. Override beforeEach/afterEach. For resource management, the loan pattern (withDb { conn => ... }) is cleaner—it ensures cleanup via try/finally and makes the resource explicit. ScalaTest also supports fixture contexts (FixtureContext) and shared fixtures via traits. Prefer the loan pattern or fixture methods over mutable beforeEach state—it's more functional and avoids shared-state bugs between tests.

scala
import org.scalatest.BeforeAndAfterEach
import org.scalatest.funsuite.AnyFunSuite

class DbTest extends AnyFunSuite with BeforeAndAfterEach {
  var conn: Connection = _

  override def beforeEach(): Unit = {
    conn = DriverManager.getConnection("jdbc:h2:mem:test")
    conn.execute("CREATE TABLE users (id INT, name VARCHAR)")
  }

  override def afterEach(): Unit = {
    conn.close()
  }

  test("insert works") {
    conn.execute("INSERT INTO users VALUES (1, 'Alice')")
    val count = conn.query("SELECT COUNT(*) FROM users")
    count shouldBe 1
  }
}

// Fixture via loan pattern
class FixtureTest extends AnyFunSuite {
  def withDb(test: Connection => Unit): Unit = {
    val conn = DriverManager.getConnection("jdbc:h2:mem:test")
    try test(conn) finally conn.close()
  }

  test("query works") {
    withDb { conn =>
      conn.execute("INSERT ...")
      // assertions
    }
  }
}

Property-Based Testing (ScalaCheck)

ScalaCheck generates random test inputs, testing properties over many cases. forAll runs the property with 100 random inputs by default. whenever filters inputs. Custom Gen types constrain generated data (Gen.choose, Gen.nonEmptyListOf). This catches edge cases you'd miss with example-based tests (empty lists, negative numbers, large values). Table-driven tests (Table) are for specific cases. Property-based testing is powerful for pure functions and data transformations—define what should always be true.

scala
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Prop.forAll

class ListSpec extends AnyFunSuite with ScalaCheckPropertyChecks {
  // Property: reversing twice = identity
  test("reverse twice is identity") {
    forAll { (xs: List[Int]) =>
      xs.reverse.reverse shouldBe xs
    }
  }

  // Property with conditions
  test("head of sorted list is min") {
    forAll { (xs: List[Int]) =>
      whenever(xs.nonEmpty) {
        xs.sorted.head shouldBe xs.min
      }
    }
  }

  // Custom generators
  import org.scalacheck.Gen
  val smallInt = Gen.choose(1, 100)
  val nonEmptyList = Gen.nonEmptyListOf(smallInt)

  test("custom generator") {
    forAll(nonEmptyList) { (xs: List[Int]) =>
      xs should not be empty
      xs.forall(_ >= 1) shouldBe true
    }
  }

  // Table-driven tests
  test("addition table") {
    val cases = Table(
      ("a", "b", "sum"),
      (1, 2, 3),
      (10, 20, 30),
      (-1, 1, 0)
    )
    forAll(cases) { (a, b, sum) =>
      a + b shouldBe sum
    }
  }
}

Mocking and Test Doubles

Mockito (via ScalaTestPlus) creates test doubles. mock[T] creates a mock; when(...).thenReturn(...) stubs methods; verify checks interactions. Use mocks to isolate the unit under test from dependencies (databases, APIs). Argument matchers (argThat) verify specific arguments. Don't over-mock—if you're mocking everything, test the real integration instead. Prefer fakes (in-memory implementations) over mocks for complex dependencies—they're more robust and readable.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._

class UserServiceSpec extends AnyFunSuite with MockitoSugar {
  test("getUser returns user from repo") {
    // Create mock
    val repo = mock[UserRepository]
    val user = User(1, "Alice")

    // Stub: when X then Y
    when(repo.findById(1)).thenReturn(Some(user))

    val service = new UserService(repo)
    val result = service.getUser(1)

    result shouldBe Some(user)

    // Verify: was X called?
    verify(repo).findById(1)
    verify(repo, never()).findById(2)
  }

  test("createUser saves to repo") {
    val repo = mock[UserRepository]
    val service = new UserService(repo)

    service.createUser("Bob")

    // Verify with argument matcher
    verify(repo).save(argThat((u: User) => u.name == "Bob"))
  }
}

// Stubbing exceptions
when(repo.findById(99)).thenThrow(new RuntimeException("not found"))
17

Higher-Order Functions & FP

Functions as Values

Functions are values in Scala—you can store, pass, and return them. (A, B) => C is the function type. Eta-expansion (multiply _) converts methods to function values. Higher-order functions (taking/returning functions) enable powerful abstractions: map, filter, reduce are HOFs. Function composition (andThen, compose) builds pipelines. Currying (adder(5) returns a function) partial application. This is the foundation of functional programming in Scala.

scala
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x

// Apply
add(2, 3)  // 5
square(4)  // 16

// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _  // or just 'multiply' in Scala 3
mul(3, 4)  // 12

// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2)  // 16

// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10)  // 15

// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g  // f then g: (x+1)*2
val h2 = f compose g  // g then f: (x*2)+1
h(3)  // 8
h2(3)  // 7

Currying and Partial Application

Currying splits a function into multiple parameter lists, enabling partial application (fix some args, get a function for the rest). This aids type inference (earlier params constrain later ones) and creates configurable functions (withDb(config)). The placeholder _ partially applies: sum(1, _) creates a function. Multiple parameter lists are Scala's currying mechanism. Use currying when some args are 'configuration' and others are 'data'.

scala
// Curried function: multiple parameter lists
def add(a: Int)(b: Int): Int = a + b
val add5: Int => Int = add(5)  // partial application
add5(10)  // 15

// Multiple parameter lists for type inference
def map[A, B](list: List[A])(f: A => B): List[B] =
  list.map(f)
map(List(1, 2, 3))(x => x * 2)  // A, B inferred from first list

// Curried form (Function types)
val curriedAdd: Int => Int => Int = a => b => a + b
curriedAdd(5)(10)  // 15

// Uncurrying
def uncurriedAdd(a: Int, b: Int): Int = a + b

// Practical: configuration via currying
def withDb(config: DbConfig)(f: Connection => Result): Result = ???
val withMyDb = withDb(myConfig) _
withMyDb { conn => /* ... */ }

// Partial application with placeholder
val sum: (Int, Int) => Int = _ + _
val addOne: Int => Int = sum(1, _)

Pure Functions and Referential Transparency

Pure functions always return the same output for the same input and have no side effects. They're easy to test, reason about, parallelize, and compose. Referential transparency means you can replace a function call with its result without changing behavior. Scala doesn't enforce purity, but libraries like Cats Effect (IO monad) let you isolate side effects. Pure core + IO at the edges is a common FP architecture: business logic is pure, I/O is wrapped in IO.

scala
// Pure function: same input → same output, no side effects
def pureAdd(a: Int, b: Int): Int = a + b  // pure

// Impure: depends on external state
var counter = 0
def impureAdd(a: Int): Int = { counter += 1; a + counter }  // impure

// Impure: side effect
def impurePrint(a: Int): Int = { println(a); a }  // side effect

// Referential transparency: can replace call with result
val x = pureAdd(2, 3)
val y = x + x  // same as pureAdd(2, 3) + pureAdd(2, 3)

// Benefits of purity:
// 1. Easy to test (no setup/teardown)
// 2. Easy to reason about (no hidden state)
// 3. Parallelizable (no shared state)
// 4. Memoizable (cache results)
// 5. Composable (predictable)

// IO monad for side effects (Cats Effect)
import cats.effect.IO
val program: IO[Unit] = IO.println("hello")
val mapped: IO[String] = IO.pure("world").map(_.toUpperCase)
// Side effects captured in IO, run at edge of program

Immutable Data and Persistent Collections

Immutable data structures return new copies on modification, sharing structure internally for efficiency (persistent data structures). Scala's collections are immutable by default. case class copy creates modified copies. For deep updates, Lens libraries (Monocle) provide composable accessors. Immutability eliminates whole classes of bugs (race conditions, unexpected mutations) and makes code easier to reason about. The performance cost is often acceptable due to structural sharing (O(log n) not O(n)).

scala
// Immutable: operations return new collections
val list1 = List(1, 2, 3)
val list2 = list1 :+ 4  // List(1, 2, 3, 4)
// list1 is unchanged: List(1, 2, 3)

// Persistent data structures: share structure (efficient)
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3)  // shares structure with map1
// O(log n) due to structural sharing, not O(n) copy

// Case classes: copy for modification
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // new instance, alice unchanged

// Lens (Monocle library) for nested updates
import monocle.macros.GenLens
val ageLens = GenLens[User](_.age)
val updated = ageLens.modify(_ + 1)(alice)  // User("Alice", 31)

// Benefits:
// - No bugs from shared mutation
// - Easy to reason about (values don't change)
// - Free concurrency (no locks needed)
// - Undo/redo trivial (keep old versions)

Recursion and Tail Calls

Tail recursion (where the recursive call is the last operation) is optimized by the Scala compiler to a loop—no stack growth. @tailrec verifies this at compile time. The accumulator pattern (passing accumulated result) converts non-tail recursion to tail recursion. foldLeft/foldRight encapsulate common recursion patterns. For deep non-tail recursion, use trampolines (Cats) or rewrite with folds. Prefer folds over explicit recursion for clarity—they're tail-recursive and idiomatic.

scala
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
  if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000)  // StackOverflowError!

// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
  if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000)  // works (no stack overflow)

// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive

// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap

// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally

// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
  list match
    case Nil => acc
    case head :: tail => reverse(tail, head :: acc)
18

Case Classes & ADTs

Case Class Basics

Case classes are Scala's primary data modeling tool. The compiler generates equals, hashCode, toString, copy, apply, unapply (for pattern matching), and accessor methods. They're immutable by default (val fields). Use case classes for DTOs, value objects, messages, and domain entities (when immutable). The companion object's apply lets you construct without 'new'. copy enables non-destructive updates. Case classes are the foundation of ADTs (Algebraic Data Types).

scala
// Case class: immutable data holder with boilerplate generated
case class Person(name: String, age: Int)

val alice = Person("Alice", 30)
val bob = Person("Bob", 25)

// Auto-generated methods:
alice.toString  // "Person(Alice,30)"
alice == Person("Alice", 30)  // true (structural equality)
alice.hashCode  // based on fields

// Pattern matching
alice match
  case Person(name, age) => s"$name is $age"

// Copy with modifications
val older = alice.copy(age = 31)  // Person("Alice", 31)

// Companion object with apply (no 'new' needed)
val p = Person("Charlie", 40)  // Person.apply called

// Fields accessed directly
alice.name  // "Alice"
alice.age   // 30

// Case class with default values
case class Point(x: Double = 0, y: Double = 0)
Point()  // Point(0.0, 0.0)
Point(y = 5)  // Point(0.0, 5.0)

Sealed Traits and ADTs

ADTs (Algebraic Data Types) model data as a closed set of cases (sealed trait + case classes/objects). 'sealed' means all subtypes are in the same file, enabling exhaustive pattern matching—the compiler warns if you miss a case. Adding a new case shows all places needing updates (safe refactoring). Case objects are singletons (no params). Option, List, Either are all ADTs. This is the functional way to model domains—each variant is a case, behavior is in pattern matching.

scala
// ADT: sealed trait + case classes/objects
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(a: Double, b: Double, c: Double) extends Shape

// Exhaustive pattern matching (compiler checks all cases)
def area(shape: Shape): Double = shape match
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
  case Triangle(a, b, c) =>
    val s = (a + b + c) / 2
    math.sqrt(s * (s-a) * (s-b) * (s-c))

// Adding a new case: compiler warns about non-exhaustive matches
case class Square(side: Double) extends Shape
// area now needs a Square case!

// Case objects for singletons (no parameters)
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status

// Option and List are ADTs:
// sealed trait Option[+A]
// case class Some[A](value: A) extends Option[A]
// case object None extends Option[Nothing]

Case Class Features

Case classes can have methods, custom apply (for validation), implement traits, and be generic. Custom apply in the companion object can validate before construction (make illegal states unrepresentable). Case classes can implement traits for polymorphism. The private constructor (case class Email private) forces using the companion's apply for validation. This combines data modeling with encapsulation—constructors that can't fail silently.

scala
// Case class with methods
case class Vec2(x: Double, y: Double) {
  def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
  def magnitude: Double = math.sqrt(x*x + y*y)
  def dot(other: Vec2): Double = x*other.x + y*other.y
}

Vec2(1, 2) + Vec2(3, 4)  // Vec2(4.0, 6.0)
Vec2(3, 4).magnitude  // 5.0

// Case class with custom apply (validation)
case class Email private (value: String)
object Email {
  def apply(value: String): Email = {
    require(value.contains("@"), "invalid email")
    new Email(value)  // bypass public apply
  }
}
// Email("notanemail")  // IllegalArgumentException
Email("[email protected]")  // works

// Case class implementing trait
trait Jsonable {
  def toJson: String
}
case class User(name: String, age: Int) extends Jsonable {
  def toJson: String = s"""{"name":"$name","age":$age}"""
}

// Case class with type parameters
case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
}

Pattern Matching Deep Dive

Pattern matching is Scala's powerful destructuring tool. It supports: literal patterns (0, "hello"), type patterns (s: String), case class patterns (User(name, age)), guards (if condition), nested patterns, and binding (@). The compiler checks exhaustiveness for sealed types. Patterns are tried top-to-bottom. Use @ to bind the whole value while extracting parts. Pattern matching replaces if-else chains, type checks, and destructuring with one unified, readable syntax.

scala
// Various pattern types
val x: Any = (1, "hello")

x match
  case (a: Int, b: String) => s"int $a, string $b"
  case (a, b) => s"pair: $a, $b"
  case _ => "other"

// Guards
def classify(n: Int): String = n match
  case n if n < 0 => "negative"
  case 0 => "zero"
  case n if n % 2 == 0 => "even positive"
  case _ => "odd positive"

// Case class patterns (nested)
case class Point(x: Int, y: Int)
case class Shape(center: Point, size: Int)

def isOrigin(s: Shape): Boolean = s match
  case Shape(Point(0, 0), _) => true
  case _ => false

// Named patterns (bind the whole while extracting)
case class User(name: String, age: Int)
def describe(u: User): String = u match
  case u @ User(name, age) if age < 18 => s"$name is a minor"
  case User(name, _) => s"$name is an adult"

// Type patterns
def handle(x: Any): String = x match
  case s: String => s"string: $s"
  case n: Int => s"int: $n"
  case list: List[_] => s"list of size ${list.size}"
  case None => "none"
  case _ => "unknown"

Extractors and Custom Patterns

Extractors (unapply) enable pattern matching on any type, not just case classes. This decouples the matching pattern from the data representation. Regexes are extractors (groups become bindings). You can write extractors for external types (JSON, URLs) without modifying them. unapply returns Option[(T1, T2, ...)] for extraction, or Boolean for simple matching. Extractors make pattern matching extensible to any data source. Case classes auto-generate unapply; custom extractors add matching for non-case-class types.

scala
// Custom extractor via unapply
object Email {
  def unapply(str: String): Option[(String, String)] = {
    val parts = str.split("@")
    if (parts.length == 2) Some((parts(0), parts(1)))
    else None
  }
}

// Use in pattern matching
"[email protected]" match
  case Email(user, domain) => s"user: $user, domain: $domain"
  case _ => "not an email"

// Boolean extractor (no extracted values)
object Even {
  def unapply(n: Int): Boolean = n % 2 == 0
}

5 match
  case Even() => "even"
  case _ => "odd"

// Extractor with variable arity
object Pair {
  def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t._1, t._2)
}

// Regex as extractor
val Date = "(\\d{4})-(\\d{2})-(\\d{2})".r
"2024-01-15" match
  case Date(year, month, day) => s"$year/$month/$day"
  case _ => "not a date"

// Practical: parse without case classes
object Json {
  def unapply(s: String): Option[Any] =
    scala.util.Try(ujson.read(s)).toOption
}
19

Collections Deep

Immutable Collections

Scala collections are immutable by default. List is a linked list (O(n) random access). Vector is a tree with O(log n) access. Map and Set are hash-based. Operations return new collections, sharing structure for efficiency.

scala
val list = List(1, 2, 3)
val vector = Vector(1, 2, 3)  // Fast random access
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
// All immutable: operations return new collections
val updated = map + ("c" -> 3)  // New map

Collection Operations

map transforms elements. filter selects. reduce combines. grouped chunks. flatten merges nested collections. flatMap maps and flattens. All return new collections. Lazy collections use .view or .iterator for efficiency.

scala
val nums = (1 to 10).toList
val doubled = nums.map(_ * 2)
val evens = nums.filter(_ % 2 == 0)
val sum = nums.reduce(_ + _)
val grouped = nums.grouped(3).toList  // List(List(1,2,3), List(4,5,6), ...)
val flat = List(List(1,2), List(3,4)).flatten  // List(1,2,3,4)

Pattern Matching Collections

Pattern matching works on collections. :: (cons) matches head and tail. List(a, b, c) matches exactly 3 elements. Nil matches empty list. _ is wildcard. Useful for parsing and destructuring. Exhaustive matching prevents bugs.

scala
val list = List(1, 2, 3)
list match {
    case head :: tail => println(s"Head: $head")
    case Nil => println("Empty")
}
List(1, 2, 3) match {
    case List(a, b, c) => println(s"$a, $b, $c")
    case _ => println("Other")
}

Mutable Collections

Mutable collections (ArrayBuffer, mutable.Set, mutable.Map) modify in place. Faster for frequent updates but not thread-safe. Use when performance matters and immutability is not needed. Convert to immutable with .toMap, .toSet.

scala
import scala.collection.mutable
val buffer = mutable.ArrayBuffer(1, 2, 3)
buffer += 4  // Add element
buffer -= 2  // Remove element
buffer(0) = 10  // Update
val mset = mutable.Set(1, 2, 3)
val mmap = mutable.Map("a" -> 1)
mmap("b") = 2

Lazy Collections

view creates a lazy view: operations are deferred until forced. Useful for chained operations on large collections. LazyList (Scala 2.13+) is a lazy sequence. Infinite sequences are possible with lazy evaluation. Force with .toList, .toArray.

scala
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head  // Only computes first
val result = lazyView.take(10).toList  // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
20

Implicits

Implicit Parameters

Implicit parameters are passed automatically when in scope. The compiler searches for a matching implicit value. Used for configuration, type classes, and context. Can be overridden explicitly. Multiple implicits must have distinct types.

scala
def connect(url: String)(implicit timeout: Int): Unit = {
    println(s"Connecting to $url with timeout $timeout")
}
implicit val defaultTimeout: Int = 5000
connect("http://example.com")  // Uses 5000
connect("http://example.com")(3000)  // Explicit override

Implicit Conversions

Implicit conversions automatically convert types. Can be dangerous (unexpected conversions). implicit class adds extension methods. Scala 3 uses given/using and extension for clarity. Prefer extension methods over raw conversions.

scala
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
    def squared: Int = n * n
}
5.squared  // 25

Type Classes

Type classes are a pattern for ad-hoc polymorphism. A trait defines behavior, instances provide implementations for specific types. Implicit resolution finds the instance. More flexible than inheritance. Common in Cats and Shapeless.

scala
trait Show[A] {
    def show(a: A): String
}
object Show {
    implicit val intShow: Show[Int] = (a: Int) => a.toString
    implicit val stringShow: Show[String] = (a: String) => a
    def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42)  // "42"
Show("hello")  // "hello"

Context Bounds

Context bounds [A: TypeClass] are syntactic sugar for implicit parameters. The type class instance is available via implicitly. Cleaner syntax for type class constraints. Common with Ordering, Numeric, and custom type classes.

scala
def max[A: Ordering](a: A, b: A): A = {
    val ord = implicitly[Ordering[A]]
    if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
    if (ord.gt(a, b)) a else b

Given/Using (Scala 3)

Scala 3 replaces implicits with given/using for clarity. given defines an instance, using declares a parameter. extension replaces implicit class. More explicit and readable. Migration tools convert Scala 2 implicits.

scala
// Scala 3 syntax
given defaultTimeout: Int = 5000
def connect(url: String)(using timeout: Int): Unit =
    println(s"Connecting with $timeout")
connect("http://example.com")  // Uses given
// Extension methods
extension (n: Int)
    def squared: Int = n * n
21

Concurrency

Future

Future represents an async computation. onComplete handles completion. map/flatMap chain operations. ExecutionContext provides threads. Futures are immutable and one-shot. Use for-comprehension for multiple futures.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
    Thread.sleep(1000)
    42
}
f.onComplete {
    case Success(v) => println(v)
    case Failure(e) => println(e)
}
// Map/flatMap for chaining
val f2 = f.map(_ * 2).flatMap(x => Future(x + 1))

for-comprehension with Futures

for-comprehension desugars to flatMap/map. Sequential: each step waits for the previous. For parallel execution, start futures before the for. Much more readable than nested callbacks. Works with any monad (Future, Option, List).

scala
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
    user <- getUser(1)
    orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chaining

Parallel Futures

Starting futures before the for-comprehension runs them in parallel. Future.sequence converts List[Future[T]] to Future[List[T]]. Future.traverse maps and sequences in one step. zip combines two futures. All complete when the slowest finishes.

scala
val f1 = Future { compute1() }
val f2 = Future { compute2() }
val combined: Future[(Int, Int)] = for {
    r1 <- f1
    r2 <- f2
} yield (r1, r2)
// Or: Future.sequence(List(f1, f2))
// Or: Future.traverse(list)(compute)

Actors (Akka)

Akka actors encapsulate state and communicate via messages. No shared mutable state. Each actor processes one message at a time. ! (tell) sends fire-and-forget. ? (ask) returns a Future. Supervision handles failures. Ideal for concurrent stateful systems.

scala
import akka.actor.*
class Counter extends Actor {
    var count = 0
    def receive = {
        case "inc" => count += 1
        case "get" => sender() ! count
    }
}
val system = ActorSystem("mySystem")
val counter = system.actorOf(Props[Counter], "counter")
counter ! "inc"
counter ! "get"

Cats Effect IO

Cats Effect IO is a pure functional IO monad. Referentially transparent: IO(println("x")) is a value. Composes with for-comprehension. Cancellable and resource-safe. unsafeRunSync runs at the edge of your program. Alternative to Future with better semantics.

scala
import cats.effect.IO
val program: IO[Int] = for {
    _ <- IO(println("Start"))
    result <- IO.pure(42)
    _ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellable
22

Pattern Matching Deep

Case Classes

Case classes auto-generate equals, hashCode, toString, and apply/unapply. Perfect for pattern matching. Pattern matching destructures them. Immutable by default. Use for algebraic data types. copy() creates modified copies.

scala
case class Point(x: Int, y: Int)
val p = Point(1, 2)
p match {
    case Point(0, 0) => "origin"
    case Point(0, _) => "on y-axis"
    case Point(x, 0) => s"on x-axis at $x"
    case Point(x, y) => s"at ($x, $y)"
}

Sealed Traits

sealed traits restrict subtypes to the same file. The compiler checks exhaustiveness in pattern matching. Adding a new subtype causes warnings in all matches. Ideal for closed type hierarchies. Combined with case classes for ADTs.

scala
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
    case Circle(r) => math.Pi * r * r
    case Square(s) => s * s
    case Rectangle(w, h) => w * h
}  // Compiler warns if not exhaustive

Guards & Extractors

Guards (if) add conditions to patterns. Custom extractors (unapply) enable pattern matching on any type. unapply returns Option to indicate match. Extractors decouple matching from the type. Powerful for DSLs and parsing.

scala
def classify(n: Int): String = n match {
    case x if x < 0 => "negative"
    case 0 => "zero"
    case x if x % 2 == 0 => "even"
    case _ => "odd"
}
// Custom extractor
object Even {
    def unapply(n: Int): Option[Int] =
        if (n % 2 == 0) Some(n / 2) else None
}
4 match { case Even(half) => s"half is $half" }

Partial Functions

Partial functions are defined only for some inputs. isDefinedAt checks. collect applies only where defined. Useful for callbacks and routing. orElse combines partial functions. lift converts to total function returning Option.

scala
val pf: PartialFunction[Int, String] = {
    case 1 => "one"
    case 2 => "two"
}
pf.isDefinedAt(1)  // true
pf.isDefinedAt(3)  // false
// Collect = filter + map
List(1, 2, 3, 1).collect(pf)  // List("one", "two", "one")

Pattern Matching Types

Pattern matching works on types, but type erasure affects generics. List(a, b) matches a 2-element list. List(_*) matches any list. Avoid matching on generic types like List[Int] (erased). Use type tags for runtime type info.

scala
def describe(x: Any): String = x match {
    case i: Int => s"Int: $i"
    case s: String => s"String: $s"
    case List(a, b) => s"Two-element list: $a, $b"
    case List(_*) => "List with elements"
    case Some(v) => s"Some: $v"
    case None => "None"
    case _ => "Unknown"
}
23

Functional Programming

Higher-Order Functions

Higher-order functions take or return functions. Currying splits multi-arg functions into single-arg chains. Partial application fixes some arguments. Enables function composition and reuse. _ creates a partially applied function.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5)  // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _  // Partial application
add5(3)  // 8

Option & Either

Option represents optional values: Some or None. Avoids null. getOrElse provides default. Either represents success (Right) or failure (Left). Better than exceptions for expected errors. Both are monads: map, flatMap, for-comprehension.

scala
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
find(1).getOrElse("Unknown")  // "Alice"
find(-1).getOrElse("Unknown")  // "Unknown"
// Either for error handling
def parse(s: String): Either[String, Int] =
    try Right(s.toInt)
    catch { case _: Exception => Left(s"Not a number: $s") }

Function Composition

compose chains functions right to left (like math). andThen chains left to right (more readable). Both create new functions. Useful for building pipelines. Functions are first-class values in Scala.

scala
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
// Compose (right to left)
val f = addOne compose double  // double then addOne
f(3)  // 7
// AndThen (left to right)
val g = addOne andThen double  // addOne then double
g(3)  // 8

Recursion & Tail Recursion

Tail recursion is optimized to a loop by the compiler. The recursive call must be the last operation. @tailrec annotation verifies this at compile time. Accumulator pattern carries state. Prevents stack overflow for deep recursion.

scala
// Not tail-recursive: stack overflow for large n
def factorial(n: Int): Int =
    if (n <= 1) 1 else n * factorial(n - 1)
// Tail-recursive: optimized to loop
import scala.annotation.tailrec
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

Monads

Monads have flatMap and unit (pure/pure). They chain operations with context (Option: absence, List: non-determinism, Future: async). for-comprehension desugars to flatMap/map. Monad laws ensure correct composition. Cats provides Monad type class.

scala
// Monad laws: left identity, right identity, associativity
// Option is a monad:
Some(5).flatMap(x => Some(x + 1))  // Some(6)
None.flatMap(x => Some(x + 1))  // None
// List is a monad:
List(1, 2).flatMap(x => List(x, x * 10))  // List(1, 10, 2, 20)
// for-comprehension is monadic sugar:
for {
    x <- Some(5)
    y <- Some(x + 1)
} yield y  // Some(6)
24

Common Pitfalls

Null vs Option

Scala has null for Java compatibility but it is discouraged. Option explicitly represents absence. Pattern matching forces handling None. Use .toOption on Try for exceptions. Avoid null in Scala code; reserve for Java interop.

scala
// BAD: null
def find(id: Int): String =
    if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic Scala

Var vs Val

val is immutable (value), var is mutable (variable). Prefer val for safer, more predictable code. Mutable state complicates reasoning and concurrency. Use var only for local performance or when truly needed. Collections are immutable by default.

scala
var x = 1  // Mutable
x = 2  // OK
val y = 1  // Immutable
// y = 2  // Error
// Prefer val for immutability
// Use var only when necessary
// Mutable state causes bugs in concurrent code

Equality

Scala == calls equals (value equality), unlike Java. eq checks reference equality. ne is the negation of eq. Always use == for value comparison. Case classes have correct equals. For custom classes, override equals and hashCode.

scala
val a = List(1, 2)
val b = List(1, 2)
a == b  // true (value equality)
a eq b  // false (reference equality)
// In Java: a == b compares references
// In Scala: == calls equals (value)
// Use eq for reference equality (rarely needed)

Implicit Ambiguity

Multiple implicits of the same type cause ambiguity errors. Keep implicit scope clean. Use specific types (newtypes) to distinguish. Scala 3 given/using is clearer. Avoid implicit conversions; they cause surprising behavior.

scala
implicit val s1: String = "hello"
implicit val s2: String = "world"
def greet(implicit s: String) = println(s)
greet  // Error: ambiguous implicit values
// Fix: only one implicit in scope
// Or: pass explicitly
greet(s1)

By-Name vs By-Value

By-name parameters (=> T) are evaluated lazily, each time used. By-value parameters are evaluated once before the call. By-name enables custom control structures (unless, while). Can cause multiple evaluations; cache with lazy val if needed.

scala
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation())  // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation())  // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
    if (!cond) body
25

Akka Actors

Actor System

ActorSystem is the container for actors. actorOf creates actors with a name. ! (tell) sends a message asynchronously. ? (ask) sends and returns a Future. Actors are identified by path. terminate shuts down the system gracefully.

scala
import akka.actor.*
val system = ActorSystem("MySystem")
val actor = system.actorOf(Props[MyActor], "myActor")
actor ! "Hello"  // Fire-and-forget
val future = actor ? "Query"  // Ask pattern
system.terminate()

Actor Lifecycle

preStart runs on creation, postStop on termination. preRestart/postRestart handle supervision. The actor is stopped before restart, then started fresh. Internal state is lost on restart. Use preStart for initialization and postStop for cleanup.

scala
class MyActor extends Actor {
    override def preStart(): Unit = println("Starting")
    override def postStop(): Unit = println("Stopped")
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        println("Restarting")
        super.preRestart(reason, message)
    }
    def receive = {
        case "ping" => sender() ! "pong"
    }
}

Supervision

Supervision defines how failures are handled. Resume: continue with same state. Restart: recreate with fresh state. Stop: terminate permanently. Escalate: let the parent handle it. OneForOneStrategy affects only the failed child. AllForOneStrategy affects all children.

scala
class Supervisor extends Actor {
    override val supervisorStrategy = OneForOneStrategy() {
        case _: ArithmeticException => Resume  // Continue
        case _: NullPointerException => Restart  // Restart actor
        case _: Exception => Stop  // Stop actor
    }
    def receive = { case _ => }
}

Routers

Routers distribute messages to multiple actors. Pool creates and manages workers. Group uses existing actors. Strategies: RoundRobin, Random, SmallestMailbox, ConsistentHashing, ScatterGatherFirst. Routers improve throughput by parallelizing work.

scala
// Pool router: creates workers
val router = system.actorOf(
    RoundRobinPool(5).props(Props[Worker]),
    "router"
)
router ! "work"  // Distributed to one of 5 workers
// Group router: uses existing actors
val group = system.actorOf(
    RoundRobinGroup(paths).props(),
    "group"
)

Persistence

PersistentActor saves events to a journal (event sourcing). Commands are validated, then persisted as events. On restart, events are replayed via receiveRecover. State is rebuilt from events. Enables recovery from crashes. Use EventSourcedBehavior for Akka Typed.

scala
class Counter extends PersistentActor {
    var count = 0
    override def persistenceId = "counter-1"
    override def receiveCommand = {
        case "inc" => persist(Incremented) { event =>
            count += 1
        }
    }
    override def receiveRecover = {
        case Incremented => count += 1
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.