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).
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.StringString 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.
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 stringTuples
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.
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.0Type 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.
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 inferredUnit & 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.
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")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.
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")) // trueMultiline 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.
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
WorldString 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).
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.
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) // 0Regex
.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.
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#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.
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.
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.
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.
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.
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, fastestControl 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.
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 neededFor 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.
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.
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.
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 immutabilityTry / 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.
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) // 0Functions
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).
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 argLambda (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.
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 parameterHigher-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.
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-likeCurrying & 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.
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.
// 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 }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.
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) // trueCase 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.
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, companionTraits (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.
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 linearizationObject (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.
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.
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 mixinsCollections & 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.
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, 5FlatMap & 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.
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.
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.
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.
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(_ + _)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.
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.
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 patternsMatching 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.
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.
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.
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")
}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.
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.
// 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.
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 scopeType 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.
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 polymorphismExtension 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.
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 * nConcurrency & 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.
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-comprehensionsComposing 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.
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.
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 collectionsPromise (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.
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 sideSync 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.
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 insteadImplicits 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.
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.
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 // convertedImplicit 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.
// 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.
// 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.
// 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
}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.
// 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.
// 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.sumCommon 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.
// 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.
// 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 correctlyType 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 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 automaticallyFor 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.
// 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, 3For 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.
// 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 compositionFor 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.
// 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.
// 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 doubledFor 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.
// 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 yieldScala 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 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.lengthUsing 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 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 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(" ").toListEnums 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 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 + 1Top-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 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"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 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.
// 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.
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.implicitConversionsType-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.
// 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 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 + 32Testing (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.
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 availableAssertions 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.
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.
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.
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.
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"))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.
// 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) // 7Currying 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'.
// 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.
// 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 programImmutable 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)).
// 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.
// 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)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).
// 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.
// 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.
// 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.
// 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.
// 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
}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.
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 mapCollection 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.
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.
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.
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") = 2Lazy 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.
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(_ + _)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.
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 overrideImplicit 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.
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 // 25Type 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.
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.
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 bGiven/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 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 * nConcurrency
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.
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).
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 chainingParallel 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.
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.
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.
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, cancellablePattern 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.
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.
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 exhaustiveGuards & 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.
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.
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.
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"
}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.
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) // 8Option & 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.
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.
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) // 8Recursion & 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.
// 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.
// 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)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.
// 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 ScalaVar 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.
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 codeEquality
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.
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.
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.
// 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) bodyAkka 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.
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.
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.
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.
// 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.
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
}
}Related Scala snippets
Copy-paste ready code for common tasks.
Pattern Matching
Destructure values and match cases in Scala.
Case Classes
Immutable data classes with auto-generated equals/hashCode/toString.
Collections Operations
Functional collection operations: map, filter, fold, groupBy.
Traits and Mixins
Compose behaviors using traits with default implementations.
Futures and Async
Asynchronous computation with Future and ExecutionContext.
Implicits (Given/Using in Scala 3)
Type-class derivation and context passing via implicits.
Akka Actors (Pekko)
Message-passing concurrency with the actor model.
Type Classes (Cats-style)
Ad-hoc polymorphism via type classes.
Was this helpful?