| name | lang-scala-dev |
| description | Foundational Scala patterns covering immutability, pattern matching, traits, case classes, for-comprehensions, and functional programming. Use when writing Scala code, understanding the type system, or needing guidance on which specialized Scala skill to use. This is the entry point for Scala development. |
Scala Fundamentals
Overview
This is the foundational skill for Scala development. Use this skill when writing Scala code, understanding core language features, or determining which specialized Scala skill to use.
Skill Hierarchy
lang-scala-dev (YOU ARE HERE - Foundational)
├── lang-scala-akka-dev (Akka actors, streams, clustering)
├── lang-scala-cats-dev (Cats FP library, type classes, effects)
├── lang-scala-zio-dev (ZIO effects, fibers, resources)
├── lang-scala-spark-dev (Apache Spark, distributed computing)
├── lang-scala-play-dev (Play Framework web applications)
└── lang-scala-testing-dev (ScalaTest, ScalaCheck, property testing)
When to Use This Skill
- Writing basic Scala code - syntax, types, control flow
- Understanding core language features - pattern matching, traits, case classes
- Learning Scala fundamentals - immutability, functional programming
- Determining skill routing - which specialized skill to use
- Troubleshooting common errors - compilation issues, type errors
Quick Reference
| Pattern | Syntax | Use Case |
|---|---|---|
| Immutable val | val x = 42 |
Default variable declaration |
| Mutable var | var x = 42 |
When mutation is necessary |
| Case class | case class User(name: String, age: Int) |
Data containers with pattern matching |
| Pattern match | x match { case ... => ... } |
Destructuring, conditional logic |
| Option | Option[A], Some(value), None |
Nullable value handling |
| Either | Either[L, R], Left(error), Right(value) |
Error handling with context |
| Try | Try { riskyOp } |
Exception handling |
| For-comprehension | for { x <- opt1; y <- opt2 } yield x + y |
Sequential monadic operations |
| Higher-order fn | list.map(f).filter(p) |
Function composition |
| Trait | trait Logging { ... } |
Interface with implementation |
| Object | object Utils { ... } |
Singleton, companion object |
| Implicit (2.x) | implicit val ord: Ordering[A] |
Type class instances |
| Given/Using (3.x) | given Ordering[A] with { ... } |
Scala 3 implicits |
Skill Routing
| Task | Use This Skill | Rationale |
|---|---|---|
| Akka actors, streams, clustering | lang-scala-akka-dev |
Specialized actor model patterns |
| Cats library, type classes, MTL | lang-scala-cats-dev |
Advanced FP abstractions |
| ZIO effects, fibers, layers | lang-scala-zio-dev |
Effect system patterns |
| Spark jobs, RDDs, DataFrames | lang-scala-spark-dev |
Distributed computing |
| Play web apps, controllers, routes | lang-scala-play-dev |
Web framework patterns |
| ScalaTest, ScalaCheck, mocking | lang-scala-testing-dev |
Testing strategies |
| Core language features | This skill | Foundational patterns |
Core Language Features
Val vs Var - Immutability
Prefer immutable val over mutable var:
// Good - immutable
val name = "Alice"
val age = 30
val user = User(name, age)
// Avoid - mutable
var counter = 0
counter += 1 // Mutation creates complexity
// Better - functional update
val counter = 0
val newCounter = counter + 1
When to use var:
// Loop counters (prefer for-comprehensions)
var i = 0
while (i < 10) {
println(i)
i += 1
}
// Mutable accumulators (prefer foldLeft)
var sum = 0
list.foreach(x => sum += x)
// Better alternatives
(0 until 10).foreach(println)
val sum = list.foldLeft(0)(_ + _)
Immutable collections:
// Immutable by default
val list = List(1, 2, 3)
val newList = list :+ 4 // Creates new list
val map = Map("a" -> 1, "b" -> 2)
val newMap = map + ("c" -> 3) // Creates new map
// Mutable collections (import required)
import scala.collection.mutable
val buffer = mutable.ListBuffer(1, 2, 3)
buffer += 4 // In-place mutation
val mutableMap = mutable.Map("a" -> 1)
mutableMap("b") = 2 // In-place mutation
Case Classes and Pattern Matching
Case classes provide automatic implementations of equals, hashCode, toString, and copy:
// Case class definition
case class User(name: String, age: Int, email: String)
// Automatic features
val user = User("Alice", 30, "alice@example.com")
println(user) // User(Alice,30,alice@example.com)
val updated = user.copy(age = 31) // Immutable update
val User(name, age, email) = user // Destructuring
Pattern matching:
// Match on case classes
def describe(user: User): String = user match {
case User("Admin", _, _) => "Administrator"
case User(name, age, _) if age < 18 => s"$name is a minor"
case User(name, age, _) => s"$name is $age years old"
}
// Match on types
def process(value: Any): String = value match {
case s: String => s.toUpperCase
case i: Int => (i * 2).toString
case d: Double => f"$d%.2f"
case _ => "Unknown"
}
// Match on collections
def sumFirst(list: List[Int]): Int = list match {
case Nil => 0
case head :: Nil => head
case head :: tail => head + sumFirst(tail)
}
// Guards and alternatives
def classify(n: Int): String = n match {
case x if x < 0 => "negative"
case 0 => "zero"
case x if x % 2 == 0 => "even positive"
case _ => "odd positive"
}
Sealed traits for ADTs:
sealed trait Result[+A]
case class Success[A](value: A) extends Result[A]
case class Failure(error: String) extends Result[Nothing]
case object Pending extends Result[Nothing]
def handle[A](result: Result[A]): String = result match {
case Success(value) => s"Got: $value"
case Failure(error) => s"Error: $error"
case Pending => "Waiting..."
// Compiler ensures exhaustiveness
}
Traits and Mixins
Traits as interfaces:
trait Logging {
def log(message: String): Unit
}
trait Auditing {
def audit(event: String): Unit
}
class Service extends Logging with Auditing {
def log(message: String): Unit = println(s"[LOG] $message")
def audit(event: String): Unit = println(s"[AUDIT] $event")
}
Traits with implementation:
trait Logging {
def log(message: String): Unit = {
println(s"${java.time.Instant.now()}: $message")
}
}
trait ErrorHandling {
def handleError(error: Throwable): Unit = {
System.err.println(s"Error: ${error.getMessage}")
}
}
class Application extends Logging with ErrorHandling {
def run(): Unit = {
log("Application starting")
try {
// Application logic
} catch {
case e: Exception => handleError(e)
}
}
}
Self-types for dependency declaration:
trait DatabaseAccess {
def query(sql: String): List[String]
}
trait UserService {
self: DatabaseAccess => // Requires DatabaseAccess
def getUsers(): List[String] = {
query("SELECT * FROM users") // Can use DatabaseAccess methods
}
}
class Application extends UserService with DatabaseAccess {
def query(sql: String): List[String] = {
// Database implementation
List("user1", "user2")
}
}
Linearization (method resolution order):
trait A { def msg = "A" }
trait B extends A { override def msg = "B" + super.msg }
trait C extends A { override def msg = "C" + super.msg }
class D extends B with C // Linearization: D -> C -> B -> A
val d = new D
println(d.msg) // "CBA"
For-Comprehensions
Desugaring to map/flatMap/filter:
// For-comprehension
val result = for {
x <- Some(1)
y <- Some(2)
z <- Some(3)
} yield x + y + z
// Desugars to
val result = Some(1).flatMap { x =>
Some(2).flatMap { y =>
Some(3).map { z =>
x + y + z
}
}
}
With filters:
val result = for {
x <- List(1, 2, 3, 4, 5)
if x % 2 == 0
y <- List(10, 20)
} yield x * y
// Desugars to
val result = List(1, 2, 3, 4, 5)
.filter(_ % 2 == 0)
.flatMap(x => List(10, 20).map(y => x * y))
// Result: List(20, 40, 40, 80)
With pattern matching:
case class User(name: String, age: Int)
val users = List(User("Alice", 30), User("Bob", 25))
val names = for {
User(name, age) <- users
if age >= 30
} yield name.toUpperCase
// Result: List("ALICE")
Combining different monadic types:
def findUser(id: Int): Option[User] = ???
def getPermissions(user: User): List[String] = ???
val result = for {
user <- findUser(123)
permission <- getPermissions(user)
} yield s"${user.name} has $permission"
// Result type: Option[List[String]]
Option, Either, Try
Option - handling nullable values:
// Creating Options
val some: Option[Int] = Some(42)
val none: Option[Int] = None
// From nullable
val maybeValue: Option[String] = Option(nullableString)
// Pattern matching
def describe(opt: Option[Int]): String = opt match {
case Some(value) => s"Got $value"
case None => "Nothing"
}
// Combinators
val doubled = some.map(_ * 2) // Some(84)
val filtered = some.filter(_ > 50) // None
val orElse = none.orElse(Some(0)) // Some(0)
val getOrElse = none.getOrElse(0) // 0
// For-comprehensions
val result = for {
x <- Some(1)
y <- Some(2)
} yield x + y // Some(3)
Either - error handling with context:
// Right for success, Left for failure
type Result[A] = Either[String, A]
def divide(a: Int, b: Int): Result[Int] = {
if (b == 0) Left("Division by zero")
else Right(a / b)
}
// Pattern matching
divide(10, 2) match {
case Right(value) => println(s"Result: $value")
case Left(error) => println(s"Error: $error")
}
// Combinators (right-biased)
val result = divide(10, 2)
.map(_ * 2) // Right(10)
.flatMap(x => divide(x, 5)) // Right(2)
// For-comprehensions
val calculation = for {
a <- divide(10, 2)
b <- divide(a, 5)
c <- divide(b, 1)
} yield c // Right(1)
Try - exception handling:
import scala.util.{Try, Success, Failure}
// Creating Try
val attempt = Try {
"123".toInt // Might throw NumberFormatException
}
// Pattern matching
attempt match {
case Success(value) => println(s"Parsed: $value")
case Failure(exception) => println(s"Error: ${exception.getMessage}")
}
// Combinators
val result = Try("123".toInt)
.map(_ * 2)
.recover { case _: NumberFormatException => 0 }
.getOrElse(-1)
// Converting to Option or Either
val opt: Option[Int] = attempt.toOption
val either: Either[Throwable, Int] = attempt.toEither
Choosing between Option, Either, Try:
| Type | Use When | Error Info |
|---|---|---|
Option[A] |
Value may be absent | No error context |
Either[E, A] |
Need error details | Custom error type E |
Try[A] |
Catching exceptions | Throwable exception |
Collections
Immutable collections (default):
// List - linked list
val list = List(1, 2, 3)
val prepended = 0 :: list // O(1) prepend
val appended = list :+ 4 // O(n) append
val concatenated = list ++ List(4, 5)
// Vector - indexed sequence
val vector = Vector(1, 2, 3)
val updated = vector.updated(1, 42) // O(log n) update
val accessed = vector(1) // O(log n) access
// Set - unique elements
val set = Set(1, 2, 3, 2) // Set(1, 2, 3)
val added = set + 4
val removed = set - 2
// Map - key-value pairs
val map = Map("a" -> 1, "b" -> 2)
val updated = map + ("c" -> 3)
val removed = map - "a"
val value = map.get("a") // Option[Int]
val valueOrDefault = map.getOrElse("z", 0)
Common operations:
val list = List(1, 2, 3, 4, 5)
// Transformations
list.map(_ * 2) // List(2, 4, 6, 8, 10)
list.filter(_ % 2 == 0) // List(2, 4)
list.flatMap(x => List(x, x * 10)) // List(1, 10, 2, 20, ...)
// Reductions
list.foldLeft(0)(_ + _) // 15
list.foldRight(0)(_ + _) // 15
list.reduce(_ + _) // 15
list.scan(0)(_ + _) // List(0, 1, 3, 6, 10, 15)
// Grouping
list.groupBy(_ % 2) // Map(0 -> List(2,4), 1 -> List(1,3,5))
list.partition(_ % 2 == 0) // (List(2, 4), List(1, 3, 5))
// Searching
list.find(_ > 3) // Some(4)
list.exists(_ > 3) // true
list.forall(_ > 0) // true
// Sorting
list.sorted // List(1, 2, 3, 4, 5)
list.sortBy(-_) // List(5, 4, 3, 2, 1)
list.sortWith(_ > _) // List(5, 4, 3, 2, 1)
// Zipping
list.zip(List("a", "b", "c")) // List((1,a), (2,b), (3,c))
list.zipWithIndex // List((1,0), (2,1), (3,2), ...)
Performance characteristics:
| Collection | Access | Prepend | Append | Update |
|---|---|---|---|---|
| List | O(n) | O(1) | O(n) | O(n) |
| Vector | O(log n) | O(log n) | O(log n) | O(log n) |
| Array | O(1) | O(n) | O(n) | O(1) |
| Set | O(log n) | O(log n) | O(log n) | - |
| Map | O(log n) | O(log n) | O(log n) | O(log n) |
Higher-Order Functions
Functions as values:
// Function literals
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x
val greet: String => Unit = name => println(s"Hello, $name")
// Method to function
def multiply(a: Int, b: Int): Int = a * b
val multiplyFn = multiply _ // Eta expansion
// Placeholder syntax
val add1 = (_: Int) + 1
val sum = (_: Int) + (_: Int)
Higher-order functions:
// Taking functions as parameters
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ * 2, 3) // 12
def repeat(n: Int)(action: => Unit): Unit = {
(1 to n).foreach(_ => action)
}
repeat(3) { println("Hello") }
// Returning functions
def multiplier(factor: Int): Int => Int = {
x => x * factor
}
val double = multiplier(2)
double(5) // 10
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _
add5(3) // 8
// Partial application
def sum3(a: Int, b: Int, c: Int): Int = a + b + c
val sumWith10 = sum3(10, _: Int, _: Int)
sumWith10(20, 30) // 60
Common higher-order patterns:
// Map, filter, fold
List(1, 2, 3)
.map(_ * 2)
.filter(_ > 3)
.foldLeft(0)(_ + _)
// Composition
val f: Int => Int = _ * 2
val g: Int => Int = _ + 1
val composed = f compose g // f(g(x))
val andThen = f andThen g // g(f(x))
composed(5) // 12 = (5 + 1) * 2
andThen(5) // 11 = (5 * 2) + 1
Implicits and Given/Using
Scala 2 implicits:
// Implicit parameters
def greet(name: String)(implicit greeting: String): String = {
s"$greeting, $name"
}
implicit val defaultGreeting: String = "Hello"
greet("Alice") // "Hello, Alice"
// Implicit conversions (use sparingly)
implicit def intToString(x: Int): String = x.toString
val s: String = 42 // Implicit conversion
// Implicit classes (extension methods)
implicit class RichInt(x: Int) {
def squared: Int = x * x
}
42.squared // 1764
// Type classes
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) => s"'$a'"
}
def print[A](a: A)(implicit s: Show[A]): Unit = {
println(s.show(a))
}
print(42) // "42"
print("hello") // "'hello'"
Scala 3 given/using:
// Given instances
trait Show[A] {
def show(a: A): String
}
given Show[Int] with {
def show(a: Int): String = a.toString
}
given Show[String] with {
def show(a: String): String = s"'$a'"
}
// Using clauses
def print[A](a: A)(using s: Show[A]): Unit = {
println(s.show(a))
}
print(42) // "42"
print("hello") // "'hello'"
// Extension methods (Scala 3)
extension (x: Int) {
def squared: Int = x * x
def cubed: Int = x * x * x
}
42.squared // 1764
Implicit resolution rules:
- Local scope - implicits defined in current scope
- Imported scope - explicitly imported implicits
- Companion objects - companion of type or type class
- Implicit scope - package objects, parent types
// Resolution example
trait Ordering[A]
object Ordering {
// Companion object - implicit scope
implicit val intOrdering: Ordering[Int] = ???
}
class MyClass {
// Local scope takes precedence
implicit val localOrdering: Ordering[Int] = ???
def sort[A](list: List[A])(implicit ord: Ordering[A]): List[A] = ???
sort(List(3, 1, 2)) // Uses localOrdering
}
Type System
Type variance:
// Covariance (+A) - subtyping preserved
trait Producer[+A] {
def produce(): A
}
class Animal
class Dog extends Animal
val dogProducer: Producer[Dog] = ???
val animalProducer: Producer[Animal] = dogProducer // OK
// Contravariance (-A) - subtyping reversed
trait Consumer[-A] {
def consume(a: A): Unit
}
val animalConsumer: Consumer[Animal] = ???
val dogConsumer: Consumer[Dog] = animalConsumer // OK
// Invariance (A) - no subtyping
trait Box[A] {
def get: A
def set(a: A): Unit
}
// List is covariant, Array is invariant
val dogs: List[Dog] = List()
val animals: List[Animal] = dogs // OK
val dogArray: Array[Dog] = Array()
// val animalArray: Array[Animal] = dogArray // Compile error
Type bounds:
// Upper bound (A <: B) - A must be subtype of B
def findMax[A <: Comparable[A]](list: List[A]): A = {
list.reduce((a, b) => if (a.compareTo(b) > 0) a else b)
}
// Lower bound (A >: B) - A must be supertype of B
sealed trait Animal
case class Dog(name: String) extends Animal
case class Cat(name: String) extends Animal
def prepend[A, B >: A](elem: B, list: List[A]): List[B] = {
elem :: list
}
val dogs: List[Dog] = List(Dog("Fido"))
val animals: List[Animal] = prepend(Cat("Whiskers"), dogs)
// Context bounds (requires implicit)
def sort[A: Ordering](list: List[A]): List[A] = {
list.sorted // Uses implicit Ordering[A]
}
// Multiple bounds
def process[A <: Animal with Comparable[A]: Show](a: A): String = ???
Type aliases and abstract types:
// Type alias
type UserId = Int
type Result[A] = Either[String, A]
val id: UserId = 123
val result: Result[Int] = Right(42)
// Abstract types
trait Container {
type Element
def add(e: Element): Unit
def get(): Element
}
class IntContainer extends Container {
type Element = Int
private var value: Int = 0
def add(e: Int): Unit = value = e
def get(): Int = value
}
// Path-dependent types
class Outer {
class Inner
def process(inner: Inner): Unit = ???
}
val outer1 = new Outer
val outer2 = new Outer
val inner1 = new outer1.Inner
// outer2.process(inner1) // Compile error - type mismatch
Common Patterns
Builder Pattern
Using copy method (case classes):
case class User(
name: String,
age: Int,
email: String,
phone: Option[String] = None,
address: Option[String] = None
)
// Building with copy
val user = User("Alice", 30, "alice@example.com")
.copy(phone = Some("555-1234"))
.copy(address = Some("123 Main St"))
Explicit builder:
class UserBuilder private (
private var name: String = "",
private var age: Int = 0,
private var email: String = "",
private var phone: Option[String] = None
) {
def withName(name: String): UserBuilder = {
this.name = name
this
}
def withAge(age: Int): UserBuilder = {
this.age = age
this
}
def withEmail(email: String): UserBuilder = {
this.email = email
this
}
def withPhone(phone: String): UserBuilder = {
this.phone = Some(phone)
this
}
def build(): User = {
require(name.nonEmpty, "Name is required")
require(email.nonEmpty, "Email is required")
User(name, age, email, phone, None)
}
}
object UserBuilder {
def apply(): UserBuilder = new UserBuilder()
}
// Usage
val user = UserBuilder()
.withName("Alice")
.withAge(30)
.withEmail("alice@example.com")
.withPhone("555-1234")
.build()
Type Classes
Definition and implementation:
// Type class definition
trait Show[A] {
def show(a: A): String
}
// Type class instances
object Show {
// Summoner method
def apply[A](implicit instance: Show[A]): Show[A] = instance
// Constructor method
def instance[A](f: A => String): Show[A] = new Show[A] {
def show(a: A): String = f(a)
}
// Instances
implicit val intShow: Show[Int] = instance(_.toString)
implicit val stringShow: Show[String] = instance(s => s"'$s'")
implicit val boolShow: Show[Boolean] = instance(_.toString)
// Derived instance
implicit def listShow[A](implicit sa: Show[A]): Show[List[A]] = {
instance(list => list.map(sa.show).mkString("[", ", ", "]"))
}
}
// Extension methods (Scala 2)
implicit class ShowOps[A](val a: A) extends AnyVal {
def show(implicit s: Show[A]): String = s.show(a)
}
// Usage
42.show // "42"
"hello".show // "'hello'"
List(1, 2, 3).show // "[1, 2, 3]"
Type class with operations:
trait Monoid[A] {
def empty: A
def combine(x: A, y: A): A
}
object Monoid {
def apply[A](implicit instance: Monoid[A]): Monoid[A] = instance
implicit val intAddMonoid: Monoid[Int] = new Monoid[Int] {
def empty: Int = 0
def combine(x: Int, y: Int): Int = x + y
}
implicit val stringMonoid: Monoid[String] = new Monoid[String] {
def empty: String = ""
def combine(x: String, y: String): String = x + y
}
implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
def empty: List[A] = Nil
def combine(x: List[A], y: List[A]): List[A] = x ++ y
}
}
def combineAll[A](list: List[A])(implicit m: Monoid[A]): A = {
list.foldLeft(m.empty)(m.combine)
}
combineAll(List(1, 2, 3, 4)) // 10
combineAll(List("a", "b", "c")) // "abc"
combineAll(List(List(1), List(2, 3))) // List(1, 2, 3)
Cake Pattern
Dependency injection using self-types:
// Component definitions
trait UserRepositoryComponent {
def userRepository: UserRepository
trait UserRepository {
def findById(id: Int): Option[User]
def save(user: User): Unit
}
}
trait EmailServiceComponent {
def emailService: EmailService
trait EmailService {
def sendEmail(to: String, subject: String, body: String): Unit
}
}
// Implementations
trait UserRepositoryComponentImpl extends UserRepositoryComponent {
def userRepository: UserRepository = new UserRepositoryImpl
class UserRepositoryImpl extends UserRepository {
def findById(id: Int): Option[User] = {
// Database access
Some(User("Alice", 30, "alice@example.com"))
}
def save(user: User): Unit = {
// Database access
println(s"Saving user: $user")
}
}
}
trait EmailServiceComponentImpl extends EmailServiceComponent {
def emailService: EmailService = new EmailServiceImpl
class EmailServiceImpl extends EmailService {
def sendEmail(to: String, subject: String, body: String): Unit = {
println(s"Sending email to $to: $subject")
}
}
}
// Application component with dependencies
trait UserServiceComponent {
self: UserRepositoryComponent with EmailServiceComponent =>
def userService: UserService = new UserServiceImpl
class UserServiceImpl extends UserService {
def registerUser(user: User): Unit = {
userRepository.save(user)
emailService.sendEmail(user.email, "Welcome", "Thanks for registering!")
}
}
trait UserService {
def registerUser(user: User): Unit
}
}
// Wiring
object Application extends UserServiceComponent
with UserRepositoryComponentImpl
with EmailServiceComponentImpl {
def main(args: Array[String]): Unit = {
val user = User("Bob", 25, "bob@example.com")
userService.registerUser(user)
}
}
Algebraic Data Types (ADTs)
Sum types (sealed traits):
// Enumeration-like ADT
sealed trait Color
case object Red extends Color
case object Green extends Color
case object Blue extends Color
// Pattern matching is exhaustive
def describe(color: Color): String = color match {
case Red => "red"
case Green => "green"
case Blue => "blue"
// Compiler ensures all cases covered
}
// ADT with data
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
def area(shape: Shape): Double = shape match {
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
case Triangle(b, h) => 0.5 * b * h
}
Product types (case classes):
// Simple product type
case class Point(x: Double, y: Double)
// Nested product types
case class Address(street: String, city: String, zip: String)
case class Person(name: String, age: Int, address: Address)
// Generic product type
case class Pair[A, B](first: A, second: B)
Combining sum and product types:
sealed trait Expression
case class Number(value: Int) extends Expression
case class Add(left: Expression, right: Expression) extends Expression
case class Multiply(left: Expression, right: Expression) extends Expression
case class Divide(left: Expression, right: Expression) extends Expression
def evaluate(expr: Expression): Either[String, Int] = expr match {
case Number(value) => Right(value)
case Add(left, right) =>
for {
l <- evaluate(left)
r <- evaluate(right)
} yield l + r
case Multiply(left, right) =>
for {
l <- evaluate(left)
r <- evaluate(right)
} yield l * r
case Divide(left, right) =>
for {
l <- evaluate(left)
r <- evaluate(right)
result <- if (r != 0) Right(l / r) else Left("Division by zero")
} yield result
}
// Usage
val expr = Divide(Add(Number(10), Number(5)), Number(3))
evaluate(expr) // Right(5)
Recursive ADTs:
sealed trait List[+A]
case object Nil extends List[Nothing]
case class Cons[A](head: A, tail: List[A]) extends List[A]
def sum(list: List[Int]): Int = list match {
case Nil => 0
case Cons(head, tail) => head + sum(tail)
}
// Binary tree
sealed trait Tree[+A]
case object Empty extends Tree[Nothing]
case class Node[A](value: A, left: Tree[A], right: Tree[A]) extends Tree[A]
def size[A](tree: Tree[A]): Int = tree match {
case Empty => 0
case Node(_, left, right) => 1 + size(left) + size(right)
}
Troubleshooting
Common Compilation Errors
Type mismatch:
// Error: type mismatch
val x: String = 42
// Fix: convert types
val x: String = 42.toString
// Error: cannot prove that A =:= B
def process[A](a: A): String = a.toString // Fine
def process[A](a: A): A = "string" // Error
// Fix: specify correct return type
def process[A](a: A): String = "string"
Missing implicit parameter:
// Error: could not find implicit value
def sort[A](list: List[A])(implicit ord: Ordering[A]): List[A] = {
list.sorted
}
sort(List(1, 2, 3)) // OK - Ordering[Int] exists
// sort(List(Person("Alice", 30))) // Error - no Ordering[Person]
// Fix: provide implicit
implicit val personOrdering: Ordering[Person] = Ordering.by(_.name)
sort(List(Person("Alice", 30))) // OK now
Pattern match not exhaustive:
sealed trait Result
case class Success(value: Int) extends Result
case class Failure(error: String) extends Result
// Warning: match may not be exhaustive
def handle(result: Result): String = result match {
case Success(value) => s"Got $value"
// Missing Failure case
}
// Fix: add all cases
def handle(result: Result): String = result match {
case Success(value) => s"Got $value"
case Failure(error) => s"Error: $error"
}
Recursive value needs type:
// Error: recursive value x needs type
val x = x + 1
// Fix: specify type
val x: Int = {
def helper: Int = helper + 1
helper
}
// Or avoid recursion
val x = 1
Variance errors:
// Error: covariant type A occurs in contravariant position
trait Producer[+A] {
def produce(): A // OK - covariant position
// def consume(a: A): Unit // Error - contravariant position
}
// Fix: use lower bound
trait Producer[+A] {
def produce(): A
def consume[B >: A](b: B): Unit // OK
}
Runtime Issues
NullPointerException:
// Dangerous - nullable
val name: String = null
// name.toUpperCase // NullPointerException
// Better - use Option
val name: Option[String] = None
name.map(_.toUpperCase) // Safe
StackOverflowError in recursion:
// Not tail recursive
def factorial(n: Int): Int = {
if (n <= 1) 1
else n * factorial(n - 1) // Not in tail position
}
// factorial(10000) // StackOverflowError
// Fix: use tail recursion
@scala.annotation.tailrec
def factorial(n: Int, acc: Int = 1): Int = {
if (n <= 1) acc
else factorial(n - 1, n * acc) // Tail call
}
factorial(10000) // OK
ClassCastException:
// Dangerous - type erasure
def castList(list: Any): List[Int] = list.asInstanceOf[List[Int]]
val stringList = List("a", "b", "c")
val intList = castList(stringList) // No error yet
// intList.head + 1 // ClassCastException at runtime
// Better - use pattern matching
def safeToIntList(value: Any): Option[List[Int]] = value match {
case list: List[_] if list.forall(_.isInstanceOf[Int]) =>
Some(list.asInstanceOf[List[Int]])
case _ => None
}
Performance Tips
Prefer immutable collections:
// Immutable operations create new instances
val list = List(1, 2, 3)
val newList = list :+ 4 // Structural sharing, efficient
// For building, use builders
val builder = List.newBuilder[Int]
(1 to 1000).foreach(builder += _)
val result = builder.result()
Use tail recursion:
// Stack-safe tail recursion
@scala.annotation.tailrec
def sum(list: List[Int], acc: Int = 0): Int = list match {
case Nil => acc
case head :: tail => sum(tail, acc + head)
}
Avoid unnecessary Option wrapping:
// Inefficient
val result = Some(value).map(transform).getOrElse(default)
// Better
val result = if (condition) transform(value) else default
Use views for large collections:
// Eager evaluation - multiple passes
val result = list.map(_ * 2).filter(_ > 10).take(5)
// Lazy evaluation - single pass
val result = list.view.map(_ * 2).filter(_ > 10).take(5).toList
Best Practices
Code Organization
- Use package objects for package-level definitions:
package com.example
package object utils {
type UserId = Int
type Result[A] = Either[String, A]
implicit class StringOps(s: String) {
def toUserId: UserId = s.toInt
}
}
- Companion objects for factory methods:
case class User private (name: String, age: Int)
object User {
def create(name: String, age: Int): Either[String, User] = {
if (age < 0) Left("Age must be positive")
else if (name.isEmpty) Left("Name cannot be empty")
else Right(new User(name, age))
}
}
- Sealed traits in same file:
// All implementations must be in this file
sealed trait Result[+A]
case class Success[A](value: A) extends Result[A]
case class Failure(error: String) extends Result[Nothing]
case object Pending extends Result[Nothing]
Naming Conventions
- Classes/Traits: PascalCase (
UserService,HttpClient) - Objects: PascalCase (
DatabaseConfig) - Methods/Values: camelCase (
findUser,maxRetries) - Type parameters: Single uppercase letter (
A,B,T) - Implicits: Descriptive names (
userOrdering,jsonEncoder)
Error Handling
Prefer typed errors over exceptions:
// Good
sealed trait UserError
case object UserNotFound extends UserError
case object InvalidEmail extends UserError
def findUser(id: Int): Either[UserError, User] = ???
// Avoid
def findUser(id: Int): User = {
throw new UserNotFoundException(s"User $id not found")
}
Concurrency
Scala provides multiple concurrency models: Futures for simple async operations, Akka actors for complex concurrent systems, and modern effect systems like Cats Effect and ZIO.
Futures - Basic Async
Creating and using Futures:
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import scala.concurrent.ExecutionContext.Implicits.global
// Create Future
val future = Future {
Thread.sleep(1000)
42
}
// Transform with map
val doubled = future.map(_ * 2)
// Chain with flatMap
val chained = future.flatMap { value =>
Future(value + 10)
}
// For-comprehension
val result = for {
a <- Future(10)
b <- Future(20)
c <- Future(30)
} yield a + b + c
// Blocking (avoid in production)
val value = Await.result(future, 5.seconds)
Combining Futures:
// Sequence - converts List[Future[A]] to Future[List[A]]
val futures = List(Future(1), Future(2), Future(3))
val combined: Future[List[Int]] = Future.sequence(futures)
// Traverse - map then sequence
val ids = List(1, 2, 3)
val users: Future[List[User]] = Future.traverse(ids)(id => fetchUser(id))
// First completed
val fastest = Future.firstCompletedOf(List(
fetchFromPrimary(),
fetchFromBackup()
))
// Recover from failures
val safe = future.recover {
case _: TimeoutException => 0
case _: Exception => -1
}
val safeWith = future.recoverWith {
case _: Exception => fetchFromCache()
}
Promise - explicit completion:
import scala.concurrent.Promise
val promise = Promise[Int]()
val future = promise.future
// Complete in another thread
Future {
Thread.sleep(1000)
promise.success(42)
}
// Or fail
promise.failure(new Exception("Failed"))
// Try complete (doesn't throw if already completed)
promise.trySuccess(100)
Akka Actors - Message Passing
Typed actors (Akka Typed):
import akka.actor.typed._
import akka.actor.typed.scaladsl.Behaviors
// Define protocol
sealed trait CounterMessage
case object Increment extends CounterMessage
case object Decrement extends CounterMessage
case class GetCount(replyTo: ActorRef[Int]) extends CounterMessage
// Define behavior
def counter(count: Int): Behavior[CounterMessage] = Behaviors.receive { (context, message) =>
message match {
case Increment =>
counter(count + 1)
case Decrement =>
counter(count - 1)
case GetCount(replyTo) =>
replyTo ! count
Behaviors.same
}
}
// Create actor system
val system = ActorSystem(counter(0), "counter-system")
// Send messages
system ! Increment
system ! Increment
Actor patterns:
// Ask pattern (request-response)
import akka.actor.typed.scaladsl.AskPattern._
import akka.util.Timeout
import scala.concurrent.duration._
implicit val timeout: Timeout = 3.seconds
val futureCount: Future[Int] = system.ask(ref => GetCount(ref))
// Supervision
def supervisedBehavior(): Behavior[String] = {
Behaviors.supervise {
Behaviors.receive[String] { (context, message) =>
if (message == "fail") throw new Exception("Failed!")
else Behaviors.same
}
}.onFailure[Exception](SupervisorStrategy.restart)
}
Cats Effect - Functional Effects
IO monad for side effects:
import cats.effect._
// Create IO
val printHello: IO[Unit] = IO.println("Hello")
val readLine: IO[String] = IO.readLine
// Compose with for-comprehension
val program = for {
_ <- IO.println("What's your name?")
name <- IO.readLine
_ <- IO.println(s"Hello, $name!")
} yield ()
// Parallel execution
import cats.effect.syntax.parallel._
import cats.syntax.apply._
val parallel = (
fetchUser(1),
fetchUser(2),
fetchUser(3)
).parMapN((u1, u2, u3) => List(u1, u2, u3))
// Resource management
def useFile(path: String): IO[String] = {
Resource.make(
IO(scala.io.Source.fromFile(path)) // Acquire
)(source => IO(source.close())) // Release
.use(source => IO(source.mkString)) // Use
}
// Error handling
val safeIO = IO.raiseError(new Exception("Error"))
.handleErrorWith(_ => IO.pure(0))
.attempt // Returns IO[Either[Throwable, Int]]
Fibers - lightweight threads:
import cats.effect.IO
def task(n: Int): IO[Unit] = IO.sleep(1.second) >> IO.println(s"Task $n")
val program = for {
fiber1 <- task(1).start // Start fiber
fiber2 <- task(2).start
_ <- fiber1.join // Wait for completion
_ <- fiber2.join
} yield ()
// Cancellation
val cancelable = for {
fiber <- IO.sleep(10.seconds).start
_ <- IO.sleep(1.second)
_ <- fiber.cancel // Cancel after 1 second
} yield ()
ZIO - Effect System
ZIO basics:
import zio._
// Create ZIO
val hello: ZIO[Any, Nothing, Unit] = ZIO.succeed(println("Hello"))
val readLine: ZIO[Any, IOException, String] = ZIO.attempt(scala.io.StdIn.readLine())
// Composition
val program = for {
_ <- Console.printLine("What's your name?")
name <- Console.readLine
_ <- Console.printLine(s"Hello, $name!")
} yield ()
// Parallel execution
val parallel = ZIO.collectAllPar(List(
fetchUser(1),
fetchUser(2),
fetchUser(3)
))
// Racing
val raced = fetchFromPrimary() race fetchFromBackup()
// Timeout
val withTimeout = fetchData().timeout(5.seconds)
ZIO Layers - dependency injection:
trait UserService {
def getUser(id: Int): ZIO[Any, Throwable, User]
}
case class UserServiceLive(database: Database) extends UserService {
def getUser(id: Int): ZIO[Any, Throwable, User] =
ZIO.attempt(database.query(s"SELECT * FROM users WHERE id = $id"))
}
object UserServiceLive {
val layer: ZLayer[Database, Nothing, UserService] =
ZLayer.fromFunction(UserServiceLive.apply _)
}
// Use the service
val program = for {
user <- ZIO.serviceWithZIO[UserService](_.getUser(123))
_ <- Console.printLine(s"User: $user")
} yield ()
// Provide dependencies
program.provide(UserServiceLive.layer, DatabaseLive.layer)
See also: patterns-concurrency-dev for cross-language concurrency comparison
Build and Dependencies
Scala uses sbt (Simple Build Tool) as the primary build tool, with Mill as a modern alternative. Dependencies are published to Maven Central and Sonatype.
sbt - Simple Build Tool
build.sbt basics:
// Project metadata
name := "my-project"
version := "0.1.0"
scalaVersion := "3.3.1"
// Organization (for publishing)
organization := "com.example"
// Dependencies
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % "2.10.0",
"org.typelevel" %% "cats-effect" % "3.5.2",
"com.lihaoyi" %% "upickle" % "3.1.3",
// Test dependencies
"org.scalatest" %% "scalatest" % "3.2.17" % Test,
"org.scalatestplus" %% "mockito-4-11" % "3.2.17.0" % Test
)
// Compiler options (Scala 3)
scalacOptions ++= Seq(
"-deprecation",
"-feature",
"-unchecked",
"-Xfatal-warnings"
)
// Resolvers (if needed)
resolvers += "Sonatype OSS Snapshots" at "https://oss.sonatype.org/content/repositories/snapshots"
Dependency syntax:
// %% - Scala version appended automatically
"org.typelevel" %% "cats-core" % "2.10.0"
// Resolves to: org.typelevel:cats-core_3:2.10.0
// % - Exact artifact name (Java libraries)
"com.google.guava" % "guava" % "32.1.3-jre"
// Cross-version dependencies
"org.scala-lang" % "scala-reflect" % scalaVersion.value
// Test scope
"org.scalatest" %% "scalatest" % "3.2.17" % Test
// Provided scope (available at compile, not packaged)
"javax.servlet" % "javax.servlet-api" % "4.0.1" % Provided
Multi-module projects:
// build.sbt root project
lazy val root = (project in file("."))
.aggregate(core, api, client)
.settings(
name := "my-project"
)
lazy val core = (project in file("core"))
.settings(
name := "my-project-core",
libraryDependencies ++= coreDeps
)
lazy val api = (project in file("api"))
.dependsOn(core)
.settings(
name := "my-project-api",
libraryDependencies ++= apiDeps
)
lazy val client = (project in file("client"))
.dependsOn(core)
.settings(
name := "my-project-client"
)
Common sbt tasks:
# Compile
sbt compile
# Run application
sbt run
# Run tests
sbt test
# Interactive mode
sbt
> compile
> test
> ~test # Watch mode - rerun on file change
# Package JAR
sbt package
# Create fat JAR (with dependencies)
sbt assembly # Requires sbt-assembly plugin
# Show dependency tree
sbt dependencyTree
# Update dependencies
sbt update
# Clean build
sbt clean
# Publish to local Ivy repository
sbt publishLocal
# Publish to Maven Central
sbt publishSigned
project/plugins.sbt - sbt plugins:
// Assembly - fat JAR
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "2.1.5")
// Coverage
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.9")
// Publishing
addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1")
addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.21")
// Formatting
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2")
// Native compilation
addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.16")
Mill - Modern Build Tool
build.sc (Mill build file):
import mill._
import mill.scalalib._
object core extends ScalaModule {
def scalaVersion = "3.3.1"
def ivyDeps = Agg(
ivy"org.typelevel::cats-core:2.10.0",
ivy"org.typelevel::cats-effect:3.5.2"
)
object test extends Tests with TestModule.ScalaTest {
def ivyDeps = Agg(
ivy"org.scalatest::scalatest:3.2.17"
)
}
}
object api extends ScalaModule {
def scalaVersion = "3.3.1"
def moduleDeps = Seq(core)
}
Mill commands:
# Compile
mill core.compile
# Run tests
mill core.test
# Run application
mill core.run
# Assembly (fat JAR)
mill core.assembly
# Watch mode
mill -w core.test
Cross-Compilation
Building for multiple Scala versions:
// build.sbt
lazy val core = (project in file("core"))
.settings(
name := "my-library",
crossScalaVersions := Seq("2.12.18", "2.13.12", "3.3.1"),
scalaVersion := "3.3.1" // Default
)
# Compile for all versions
sbt +compile
# Test all versions
sbt +test
# Publish all versions
sbt +publish
Version-specific code:
// src/main/scala-2.13/compat.scala
object Compat {
def collect[A](list: List[Option[A]]): List[A] = list.flatten
}
// src/main/scala-3/compat.scala
object Compat {
def collect[A](list: List[Option[A]]): List[A] = list.flatten
}
Publishing to Maven Central
build.sbt publishing configuration:
// Metadata
organization := "io.github.username"
homepage := Some(url("https://github.com/username/project"))
scmInfo := Some(
ScmInfo(
url("https://github.com/username/project"),
"scm:git@github.com:username/project.git"
)
)
developers := List(
Developer(
id = "username",
name = "Your Name",
email = "you@example.com",
url = url("https://github.com/username")
)
)
licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0"))
// Publishing
publishMavenStyle := true
publishTo := {
val nexus = "https://oss.sonatype.org/"
if (isSnapshot.value)
Some("snapshots" at nexus + "content/repositories/snapshots")
else
Some("releases" at nexus + "service/local/staging/deploy/maven2")
}
// PGP signing
usePgpKeyHex("YOUR_KEY_ID")
Testing
Scala has multiple testing frameworks: ScalaTest (most popular), MUnit (lightweight), specs2 (BDD-style), and ScalaCheck for property-based testing.
ScalaTest - Comprehensive Testing
Basic test suites:
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class CalculatorSpec extends AnyFlatSpec with Matchers {
"A Calculator" should "add two numbers" in {
val calc = new Calculator
calc.add(2, 3) shouldBe 5
}
it should "subtract two numbers" in {
val calc = new Calculator
calc.subtract(5, 3) shouldBe 2
}
it should "throw on division by zero" in {
val calc = new Calculator
assertThrows[ArithmeticException] {
calc.divide(10, 0)
}
}
}
Test styles:
// FlatSpec - flat, BDD-style
class UserServiceFlatSpec extends AnyFlatSpec with Matchers {
"UserService" should "find user by id" in {
// test
}
}
// FunSpec - nested describe/it
class UserServiceFunSpec extends AnyFunSpec with Matchers {
describe("UserService") {
describe("findById") {
it("should return user when found") {
// test
}
it("should return None when not found") {
// test
}
}
}
}
// WordSpec - BDD "should" style
class UserServiceWordSpec extends AnyWordSpec with Matchers {
"UserService" should {
"find user by id" in {
// test
}
"handle missing users" in {
// test
}
}
}
// FeatureSpec - acceptance testing
class UserFeatureSpec extends AnyFeatureSpec with GivenWhenThen {
Feature("User management") {
Scenario("Creating a new user") {
Given("a user registration form")
When("the user submits valid data")
Then("a new user is created")
}
}
}
Matchers:
// Equality
result shouldBe 42
result should equal(42)
result shouldEqual 42
// Comparison
value should be > 10
value should be <= 100
// Collections
list should contain(42)
list should have size 5
list shouldBe empty
list should contain allOf (1, 2, 3)
list should contain oneOf (1, 2, 3)
// Options
option shouldBe defined
option shouldBe empty
option should contain(42)
// Strings
string should startWith("Hello")
string should endWith("World")
string should include("middle")
string should fullyMatch regex "\\d+".r
// Exceptions
the [IllegalArgumentException] thrownBy {
service.process(null)
} should have message "Input cannot be null"
// Custom matchers
val beEven = be >= 0 and (x => x % 2 == 0)
value should beEven
Fixtures and lifecycle:
class DatabaseSpec extends AnyFlatSpec with Matchers with BeforeAndAfter {
var db: Database = _
before {
db = Database.connect()
db.migrate()
}
after {
db.close()
}
"Database" should "insert records" in {
db.insert(Record("test"))
db.count() shouldBe 1
}
}
// Or use BeforeAndAfterEach
class ServiceSpec extends AnyFlatSpec with BeforeAndAfterEach {
override def beforeEach(): Unit = {
// Setup before each test
}
override def afterEach(): Unit = {
// Cleanup after each test
}
}
MUnit - Lightweight Testing
MUnit basics:
import munit.FunSuite
class CalculatorSuite extends FunSuite {
test("addition works") {
assertEquals(2 + 3, 5)
}
test("division by zero fails") {
intercept[ArithmeticException] {
10 / 0
}
}
test("async operation".tag(new Tag("async"))) {
Future(42).map { result =>
assertEquals(result, 42)
}
}
}
Fixtures:
class DatabaseSuite extends FunSuite {
val db = FunFixture[Database](
setup = { _ => Database.connect() },
teardown = { db => db.close() }
)
db.test("insert works") { db =>
db.insert(Record("test"))
assertEquals(db.count(), 1)
}
}
specs2 - BDD Style
specs2 basics:
import org.specs2.mutable.Specification
class CalculatorSpec extends Specification {
"Calculator" should {
"add two numbers" in {
val calc = new Calculator
calc.add(2, 3) must_== 5
}
"handle division by zero" in {
val calc = new Calculator
calc.divide(10, 0) must throwA[ArithmeticException]
}
}
}
ScalaCheck - Property-Based Testing
Property testing basics:
import org.scalacheck.Properties
import org.scalacheck.Prop.forAll
object StringProperties extends Properties("String") {
property("reverse twice is identity") = forAll { (s: String) =>
s.reverse.reverse == s
}
property("length of concatenation") = forAll { (s1: String, s2: String) =>
(s1 + s2).length == s1.length + s2.length
}
property("startsWith") = forAll { (s: String, prefix: String) =>
(prefix + s).startsWith(prefix)
}
}
Custom generators:
import org.scalacheck.Gen
import org.scalacheck.Arbitrary
case class User(name: String, age: Int)
val genUser: Gen[User] = for {
name <- Gen.alphaStr
age <- Gen.choose(0, 120)
} yield User(name, age)
implicit val arbUser: Arbitrary[User] = Arbitrary(genUser)
property("user age is valid") = forAll { (user: User) =>
user.age >= 0 && user.age <= 120
}
Integration with ScalaTest:
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
class ListPropertiesSpec extends AnyFlatSpec with Matchers with ScalaCheckPropertyChecks {
"List" should "maintain length on reverse" in {
forAll { (list: List[Int]) =>
list.reverse.length shouldBe list.length
}
}
it should "preserve elements on sort" in {
forAll { (list: List[Int]) =>
list.sorted.toSet shouldBe list.toSet
}
}
}
Mocking
Using Mockito with ScalaTest:
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._
import org.mockito.ArgumentMatchers._
class UserServiceSpec extends AnyFlatSpec with MockitoSugar with Matchers {
"UserService" should "fetch user from repository" in {
val repo = mock[UserRepository]
val service = new UserService(repo)
val user = User("Alice", 30)
when(repo.findById(123)).thenReturn(Some(user))
val result = service.getUser(123)
result shouldBe Some(user)
verify(repo).findById(123)
}
it should "handle repository failures" in {
val repo = mock[UserRepository]
val service = new UserService(repo)
when(repo.findById(anyInt())).thenThrow(new DatabaseException("Connection failed"))
assertThrows[DatabaseException] {
service.getUser(123)
}
}
}
Using ScalaMock:
import org.scalamock.scalatest.MockFactory
class EmailServiceSpec extends AnyFlatSpec with MockFactory with Matchers {
"EmailService" should "send email via SMTP" in {
val smtp = mock[SmtpClient]
val service = new EmailService(smtp)
(smtp.send _)
.expects("alice@example.com", "Hello", "Message body")
.returning(true)
.once()
service.sendEmail("alice@example.com", "Hello", "Message body") shouldBe true
}
}
Async Testing
Testing Futures:
import org.scalatest.concurrent.ScalaFutures
import org.scalatest.time.{Seconds, Span}
class AsyncServiceSpec extends AnyFlatSpec with ScalaFutures with Matchers {
implicit val patience = PatienceConfig(timeout = Span(5, Seconds))
"AsyncService" should "fetch data asynchronously" in {
val future = service.fetchData()
whenReady(future) { result =>
result shouldBe "data"
}
}
it should "handle failures" in {
val future = service.fetchInvalid()
whenReady(future.failed) { exception =>
exception shouldBe a [NotFoundException]
}
}
}
Testing Cats Effect IO:
import cats.effect.testing.scalatest.AsyncIOSpec
class IOServiceSpec extends AsyncIOSpec with Matchers {
"IOService" should "process data" in {
service.processData().asserting { result =>
result shouldBe "processed"
}
}
it should "handle errors" in {
service.processInvalid().assertThrows[ValidationError]
}
}
Error Handling
Scala provides multiple error handling strategies, from basic Option/Either to advanced effect system error channels. Choose the right abstraction based on your needs.
Error Handling Strategies
Comparison of error handling approaches:
| Strategy | Use Case | Error Info | Composable | Stack Safe |
|---|---|---|---|---|
Option[A] |
Absence vs presence | No context | Yes | Yes |
Either[E, A] |
Typed errors | Custom error type | Yes | Yes |
Try[A] |
Exception catching | Throwable | Yes | No |
Cats EitherT |
Stacked Either/Future | Custom error type | Yes | Yes |
Cats IO |
Effect errors | Throwable | Yes | Yes |
ZIO[R, E, A] |
Effect with typed errors | Custom error type | Yes | Yes |
| Scala 3 boundary/break | Early return | Value-based | Limited | Yes |
Option - Handling Absence
Basic Option patterns:
// Creating Options
val some: Option[Int] = Some(42)
val none: Option[Int] = None
val fromNullable: Option[String] = Option(nullableValue)
// Transforming Options
val doubled = some.map(_ * 2) // Some(84)
val filtered = some.filter(_ > 50) // None
val flatMapped = some.flatMap(x => Some(x + 1)) // Some(43)
// Extracting values
val value = some.getOrElse(0) // 42
val orElse = none.orElse(Some(0)) // Some(0)
// Pattern matching
def describe(opt: Option[Int]): String = opt match {
case Some(value) if value > 0 => s"Positive: $value"
case Some(value) => s"Non-positive: $value"
case None => "No value"
}
// For-comprehension
val result = for {
a <- Some(1)
b <- Some(2)
c <- Some(3)
} yield a + b + c // Some(6)
// Short-circuits on None
val shortCircuit = for {
a <- Some(1)
b <- None // Stops here
c <- Some(3)
} yield a + b + c // None
Advanced Option patterns:
// Folding Options
val folded = some.fold(0)(_ * 2) // 42 * 2 = 84
val foldedNone = none.fold(0)(_ * 2) // 0
// Collecting from Lists
val list = List(Some(1), None, Some(3), None, Some(5))
val collected = list.flatten // List(1, 3, 5)
// Traversing Lists to Option
def safeDivide(a: Int, b: Int): Option[Int] =
if (b == 0) None else Some(a / b)
val divisions = List(10, 20, 30).traverse(safeDivide(_, 5)) // Some(List(2, 4, 6))
val failed = List(10, 20, 30).traverse(safeDivide(_, 0)) // None
// Converting to Either
val asEither: Either[String, Int] = some.toRight("No value")
val asLeft: Either[Int, String] = none.toLeft("Default")
Either - Typed Error Handling
Either basics (right-biased in Scala 2.12+):
// Creating Either values
val success: Either[String, Int] = Right(42)
val failure: Either[String, Int] = Left("Error occurred")
// Transforming Right values
val doubled = success.map(_ * 2) // Right(84)
val chained = success.flatMap(x => Right(x + 1)) // Right(43)
// Pattern matching
def handle[E, A](either: Either[E, A]): String = either match {
case Right(value) => s"Success: $value"
case Left(error) => s"Error: $error"
}
// For-comprehension (short-circuits on Left)
def divide(a: Int, b: Int): Either[String, Int] =
if (b == 0) Left("Division by zero") else Right(a / b)
val computation = for {
a <- divide(10, 2) // Right(5)
b <- divide(a, 5) // Right(1)
c <- divide(b, 1) // Right(1)
} yield c // Right(1)
val failed = for {
a <- divide(10, 2) // Right(5)
b <- divide(a, 0) // Left - stops here
c <- divide(b, 1) // Never executed
} yield c // Left("Division by zero")
Advanced Either patterns:
// Custom error types
sealed trait AppError
case class ValidationError(message: String) extends AppError
case class DatabaseError(cause: Throwable) extends AppError
case class NotFoundError(id: String) extends AppError
def findUser(id: String): Either[AppError, User] = {
if (id.isEmpty) Left(ValidationError("ID cannot be empty"))
else if (id == "999") Left(NotFoundError(id))
else Right(User(id, "Name"))
}
// Accumulating errors (requires Cats)
import cats.implicits._
def validateName(name: String): Either[String, String] =
if (name.nonEmpty) Right(name) else Left("Name is empty")
def validateAge(age: Int): Either[String, Int] =
if (age >= 0) Right(age) else Left("Age is negative")
// Sequential validation (stops at first error)
val user = for {
name <- validateName("") // Stops here
age <- validateAge(-5)
} yield User(name, age) // Left("Name is empty")
// Parallel validation (with Validated)
import cats.data.Validated
import cats.data.ValidatedNec // NonEmptyChain
def validateNameV(name: String): ValidatedNec[String, String] =
if (name.nonEmpty) Validated.validNec(name) else Validated.invalidNec("Name is empty")
def validateAgeV(age: Int): ValidatedNec[String, Int] =
if (age >= 0) Validated.validNec(age) else Validated.invalidNec("Age is negative")
val validated = (validateNameV(""), validateAgeV(-5)).mapN(User.apply)
// Invalid(NonEmptyChain("Name is empty", "Age is negative"))
// Converting to Either
validated.toEither // Left(NonEmptyChain("Name is empty", "Age is negative"))
Either combinators:
// Recovering from Left
val recovered = failure.recover {
case "specific error" => 0
}
val recoveredWith = failure.recoverWith {
case "error" => Right(0)
}
// Folding
val folded = success.fold(
error => s"Failed: $error",
value => s"Success: $value"
)
// Swapping sides
val swapped = success.swap // Left(42)
// BiMap - transform both sides
val bimapped = success.bimap(
error => s"Error: $error",
value => value * 2
)
// Filtering to Option
val filtered = success.filterOrElse(_ > 50, "Too small") // Left("Too small")
Try - Exception Handling
Try basics:
import scala.util.{Try, Success, Failure}
// Creating Try
val tryValue = Try("123".toInt) // Success(123)
val tryFailed = Try("abc".toInt) // Failure(NumberFormatException)
// Pattern matching
tryValue match {
case Success(value) => println(s"Parsed: $value")
case Failure(exception) => println(s"Failed: ${exception.getMessage}")
}
// Transforming Success
val doubled = tryValue.map(_ * 2) // Success(246)
// Chaining operations
val chained = tryValue.flatMap { value =>
Try(value / 10)
}
// For-comprehension
val computation = for {
a <- Try("10".toInt)
b <- Try("5".toInt)
c <- Try(a / b)
} yield c // Success(2)
// Short-circuits on Failure
val failed = for {
a <- Try("10".toInt)
b <- Try("abc".toInt) // Failure - stops here
c <- Try(a / b)
} yield c // Failure(NumberFormatException)
Try recovery patterns:
// Recover with default value
val recovered = tryFailed.recover {
case _: NumberFormatException => 0
}
// Recover with another Try
val recoveredWith = tryFailed.recoverWith {
case _: NumberFormatException => Try("456".toInt)
}
// Fallback to another Try
val fallback = tryFailed.orElse(Try("456".toInt))
// Converting to Option and Either
val asOption = tryValue.toOption // Some(123)
val asEither = tryValue.toEither // Right(123)
// Filtering
val filtered = tryValue.filter(_ > 100) // Success(123)
val failedFilter = tryValue.filter(_ > 200) // Failure(NoSuchElementException)
// Folding
val folded = tryValue.fold(
exception => s"Error: ${exception.getMessage}",
value => s"Success: $value"
)
Cats Effect Error Handling
IO error handling:
import cats.effect._
// Creating IO that might fail
val io: IO[Int] = IO.raiseError(new Exception("Failed"))
val successful: IO[Int] = IO.pure(42)
// Handling errors
val handled = io.handleError { error =>
println(s"Error: ${error.getMessage}")
0
}
val handledWith = io.handleErrorWith { error =>
IO.pure(0)
}
// Recovering from specific errors
val recovered = io.recover {
case _: IllegalArgumentException => 0
}
val recoveredWith = io.recoverWith {
case _: IllegalArgumentException => IO.pure(0)
}
// Attempt - convert to Either
val attempt: IO[Either[Throwable, Int]] = io.attempt
val processed = attempt.flatMap {
case Right(value) => IO.println(s"Success: $value")
case Left(error) => IO.println(s"Error: ${error.getMessage}")
}
// Redeem - handle both success and failure
val redeemed = io.redeem(
error => s"Failed: ${error.getMessage}",
value => s"Success: $value"
)
// RedeemWith - effectful version
val redeemedWith = io.redeemWith(
error => IO.pure(s"Failed: ${error.getMessage}"),
value => IO.pure(s"Success: $value")
)
// Timeout
val withTimeout = io.timeout(5.seconds)
// Retry
val retried = io.handleErrorWith { error =>
IO.sleep(1.second) >> io // Retry after delay
}
// Retry with exponential backoff (requires cats-retry)
import retry._
val policy = RetryPolicies.exponentialBackoff[IO](1.second)
val retriedWithPolicy = retryingOnAllErrors[Int](policy, onError = (_, _) => IO.unit)(io)
MonadError type class:
import cats.MonadError
import cats.syntax.all._
def safeDivide[F[_]](a: Int, b: Int)(implicit F: MonadError[F, Throwable]): F[Int] = {
if (b == 0) F.raiseError(new ArithmeticException("Division by zero"))
else F.pure(a / b)
}
// Works with any F[_] that has MonadError instance
val ioResult: IO[Int] = safeDivide[IO](10, 2)
val eitherResult: Either[Throwable, Int] = safeDivide[Either[Throwable, *]](10, 2)
// Generic error handling
def handleDivision[F[_]: MonadError[*[_], Throwable]](a: Int, b: Int): F[String] = {
safeDivide[F](a, b)
.map(result => s"Result: $result")
.handleError(error => s"Error: ${error.getMessage}")
}
ZIO Error Channel
ZIO typed errors:
import zio._
// ZIO[R, E, A] - R=environment, E=error type, A=success type
sealed trait AppError
case class ValidationError(message: String) extends AppError
case class DatabaseError(cause: Throwable) extends AppError
// Creating ZIO with typed errors
val success: ZIO[Any, AppError, Int] = ZIO.succeed(42)
val failure: ZIO[Any, AppError, Int] = ZIO.fail(ValidationError("Invalid input"))
// Handling errors
val handled = failure.catchAll { error =>
error match {
case ValidationError(msg) => ZIO.succeed(0)
case DatabaseError(cause) => ZIO.succeed(-1)
}
}
// Catching specific error types
val catchSome = failure.catchSome {
case ValidationError(msg) => ZIO.succeed(0)
}
// Converting to Either
val either: ZIO[Any, Nothing, Either[AppError, Int]] = success.either
// Fold - handle both success and failure
val folded = success.fold(
error => s"Error: $error",
value => s"Success: $value"
)
// FoldZIO - effectful version
val foldedZIO = success.foldZIO(
error => ZIO.succeed(s"Error: $error"),
value => ZIO.succeed(s"Success: $value")
)
// Mapping errors
val mappedError = failure.mapError {
case ValidationError(msg) => DatabaseError(new Exception(msg))
case other => other
}
// Retrying
val retried = failure.retry(Schedule.recurs(3))
// Retry with backoff
val retriedWithBackoff = failure.retry(
Schedule.exponential(1.second) && Schedule.recurs(5)
)
// Timeout
val withTimeout = success.timeout(5.seconds)
Error accumulation with ZIO:
import zio._
import zio.prelude.Validation
def validateName(name: String): IO[String, String] =
if (name.nonEmpty) ZIO.succeed(name) else ZIO.fail("Name is empty")
def validateAge(age: Int): IO[String, Int] =
if (age >= 0) ZIO.succeed(age) else ZIO.fail("Age is negative")
// Sequential validation (fails fast)
val sequential = for {
name <- validateName("") // Fails here
age <- validateAge(-5) // Not executed
} yield User(name, age)
// Parallel validation (accumulates errors)
val parallel = ZIO.validatePar(
validateName(""),
validateAge(-5)
)(User.apply) // Fails with both errors
// Using Validation
val validated = Validation.validateWith(
Validation.fromEither(validateName("").either),
Validation.fromEither(validateAge(-5).either)
)(User.apply)
For-Comprehension Error Propagation
Short-circuiting behavior:
// Option - short-circuits on None
val optionChain = for {
a <- Some(1)
b <- Some(2)
c <- None // Stops here
d <- Some(4) // Never executed
} yield a + b + c + d // None
// Either - short-circuits on Left
def divide(a: Int, b: Int): Either[String, Int] =
if (b == 0) Left("Division by zero") else Right(a / b)
val eitherChain = for {
a <- divide(10, 2) // Right(5)
b <- divide(a, 0) // Left - stops here
c <- divide(10, 2) // Never executed
} yield c // Left("Division by zero")
// Try - short-circuits on Failure
val tryChain = for {
a <- Try("10".toInt)
b <- Try("abc".toInt) // Failure - stops here
c <- Try("5".toInt) // Never executed
} yield a + b + c // Failure(NumberFormatException)
Mixing error types in for-comprehensions:
// Converting between types
val mixed = for {
a <- Some(10)
b <- Right(5).toOption // Convert Either to Option
c <- Try(a / b).toOption // Convert Try to Option
} yield c // Some(2)
// Using EitherT to stack Either and Future
import cats.data.EitherT
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
def findUser(id: Int): Future[Either[String, User]] = ???
def findPosts(userId: Int): Future[Either[String, List[Post]]] = ???
val result = for {
user <- EitherT(findUser(123))
posts <- EitherT(findPosts(user.id))
} yield (user, posts)
val unwrapped: Future[Either[String, (User, List[Post])]] = result.value
Scala 3 Boundary and Break
Early return with boundary/break:
import scala.util.boundary, boundary.break
// Early return from computation
def findFirst[A](list: List[A])(predicate: A => Boolean): Option[A] = {
boundary {
for (elem <- list) {
if (predicate(elem)) break(Some(elem))
}
None
}
}
findFirst(List(1, 2, 3, 4, 5))(_ > 3) // Some(4)
// Labeled boundaries
def processData(data: List[Int]): Either[String, Int] = {
boundary[Either[String, Int]] {
var sum = 0
for (value <- data) {
if (value < 0) break(Left("Negative value found"))
sum += value
}
Right(sum)
}
}
processData(List(1, 2, -3, 4)) // Left("Negative value found")
// Nested boundaries
def nestedSearch(matrix: List[List[Int]]): Option[Int] = {
boundary {
for (row <- matrix) {
boundary {
for (elem <- row) {
if (elem > 10) break(break(Some(elem))) // Break both boundaries
}
}
}
None
}
}
nestedSearch(List(List(1, 2), List(3, 15))) // Some(15)
Comparison with traditional approaches:
// Traditional approach with recursion
def findFirstRecursive[A](list: List[A])(predicate: A => Boolean): Option[A] = {
list match {
case Nil => None
case head :: tail =>
if (predicate(head)) Some(head)
else findFirstRecursive(tail)(predicate)
}
}
// Traditional approach with fold
def findFirstFold[A](list: List[A])(predicate: A => Boolean): Option[A] = {
list.foldLeft[Option[A]](None) { (acc, elem) =>
acc.orElse(if (predicate(elem)) Some(elem) else None)
}
}
// Boundary/break is more imperative and familiar
// Use when converting Java code or for performance-critical loops
Error Handling Best Practices
Choosing the right abstraction:
// Use Option for simple absence/presence
def findUser(id: Int): Option[User] = ???
// Use Either for typed errors
def validateUser(user: User): Either[ValidationError, User] = ???
// Use Try when catching exceptions
def parseJson(json: String): Try[JsonObject] = Try(Json.parse(json))
// Use IO/ZIO for effectful computations
def saveToDatabase(user: User): IO[User] = ???
// Use boundary/break for imperative-style early returns
def processLargeDataset(data: List[Data]): Result = {
boundary {
// Complex imperative logic with early returns
}
}
Error composition:
// Combining multiple error-prone operations
def registerUser(data: UserData): Either[AppError, User] = {
for {
validated <- validateUserData(data)
hashed <- hashPassword(validated.password)
user <- createUser(validated.copy(password = hashed))
_ <- sendWelcomeEmail(user)
} yield user
}
// Parallel execution with error accumulation
import cats.implicits._
def validateUserParallel(data: UserData): ValidatedNec[ValidationError, User] = {
(
validateName(data.name),
validateEmail(data.email),
validateAge(data.age)
).mapN(User.apply)
}
Metaprogramming
Scala provides powerful metaprogramming capabilities, evolving from Scala 2's macro system to Scala 3's safer and more principled approach with inline, transparent inline, and the new macro system.
Metaprogramming Evolution
Scala 2 vs Scala 3 metaprogramming:
| Feature | Scala 2 | Scala 3 |
|---|---|---|
| Macros | def macros (blackbox/whitebox) | inline + quoted code |
| Compile-time | Limited | Rich compile-time operations |
| Type-level | Shapeless library | Built-in match types, unions |
| Derivation | Manual implicit derivation | derives keyword |
| Safety | Hygiene issues possible | Hygienic by default |
| Complexity | High learning curve | More principled |
Scala 2 Macros (Legacy)
Def macros (avoid in new code):
// Scala 2 blackbox macro example
import scala.language.experimental.macros
import scala.reflect.macros.blackbox.Context
object DebugMacros {
def debug(value: Any): Unit = macro debugImpl
def debugImpl(c: Context)(value: c.Expr[Any]): c.Expr[Unit] = {
import c.universe._
val valueTree = value.tree
val valueString = show(valueTree)
reify {
println(s"$valueString = ${value.splice}")
}
}
}
// Usage
val x = 42
DebugMacros.debug(x + 10) // Prints: "x + 10 = 52"
// Whitebox macro - can refine return type
def mkArray[T](elements: T*): Array[T] = macro mkArrayImpl[T]
def mkArrayImpl[T: c.WeakTypeTag](c: Context)(elements: c.Expr[T]*): c.Expr[Array[T]] = {
import c.universe._
c.Expr[Array[T]](
q"Array(..${elements.map(_.tree)})"
)
}
Macro bundles (Scala 2):
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
trait JsonMacros {
val c: blackbox.Context
import c.universe._
def encodeImpl[T: c.WeakTypeTag]: c.Expr[JsonEncoder[T]] = {
val tpe = weakTypeOf[T]
val fields = tpe.decls.collect {
case m: MethodSymbol if m.isCaseAccessor =>
val name = m.name.toString
val returnType = m.returnType
q"($name, encode(value.$m))"
}
c.Expr[JsonEncoder[T]](q"""
new JsonEncoder[$tpe] {
def encode(value: $tpe): Json = Json.obj(..$fields)
}
""")
}
}
object JsonEncoder {
def derived[T]: JsonEncoder[T] = macro JsonMacrosImpl.encodeImpl[T]
}
class JsonMacrosImpl(val c: blackbox.Context) extends JsonMacros
Scala 3 Inline
Inline definitions:
// Simple inline method
inline def square(x: Int): Int = x * x
// Compiler inlines the code at call site:
val result = square(5) // Becomes: val result = 5 * 5
// Inline parameters
inline def repeat(inline n: Int)(body: => Unit): Unit = {
if (n > 0) {
body
repeat(n - 1)(body)
}
}
repeat(3)(println("Hello"))
// Expands to:
// println("Hello")
// println("Hello")
// println("Hello")
// Inline values
inline val DEBUG = true
inline def log(msg: String): Unit = {
if (DEBUG) println(s"[DEBUG] $msg")
// If DEBUG is false, entire if-statement is eliminated
}
Inline match (type-based specialization):
inline def process[T](value: T): String = inline value match {
case _: String => s"String: $value"
case _: Int => s"Int: $value"
case _: Boolean => s"Boolean: $value"
case _ => s"Other: $value"
}
// Specialized at compile time
process("hello") // String: hello
process(42) // Int: 42
// Inline match on types
inline def defaultValue[T]: T = inline erasedValue[T] match {
case _: Int => 0.asInstanceOf[T]
case _: String => "".asInstanceOf[T]
case _: Boolean => false.asInstanceOf[T]
case _ => null.asInstanceOf[T]
}
Compile-time operations:
import scala.compiletime._
// Compile-time error
inline def requirePositive(inline x: Int): Int = {
inline if (x <= 0) {
error("Value must be positive")
}
x
}
val good = requirePositive(5) // OK
// val bad = requirePositive(-1) // Compile error: Value must be positive
// Compile-time assertions
inline def assertType[T, U](value: T): Unit = {
inline if (!constValue[T =:= U]) {
error("Type mismatch")
}
}
// Summon implicits
inline def summonAll[T <: Tuple]: List[Any] = inline erasedValue[T] match {
case _: EmptyTuple => Nil
case _: (t *: ts) => summonInline[t] :: summonAll[ts]
}
Transparent Inline
Transparent inline for type refinement:
// Regular inline - return type is fixed
inline def choose(inline b: Boolean, x: Int, y: String): Any =
if (b) x else y
val result1 = choose(true, 42, "hello") // Type: Any
// Transparent inline - return type is refined
transparent inline def chooseT(inline b: Boolean, x: Int, y: String) =
if (b) x else y
val result2 = chooseT(true, 42, "hello") // Type: Int
val result3 = chooseT(false, 42, "hello") // Type: String
// Generic transparent inline
transparent inline def transformOrKeep[T](inline transform: Boolean, value: T) =
inline if (transform) {
value.toString
} else {
value
}
val a = transformOrKeep(true, 42) // Type: String
val b = transformOrKeep(false, 42) // Type: Int
Type-level computations:
// Transparent inline for type-level arithmetic
transparent inline def toIntSingleton(inline x: Int): x.type = x
val five = toIntSingleton(5) // Type: 5
// Tuple operations
transparent inline def head[T <: Tuple](t: T): Any = inline t match {
case t: (h *: tail) => t.head
}
val tuple = (1, "hello", true)
val h = head(tuple) // Type: Int (refined!)
// Dependent types via transparent inline
trait Size[N <: Int]
transparent inline def sizedArray[N <: Int](inline n: N): Array[Int] = {
new Array[Int](n)
}
Scala 3 Macros
Quote and splice:
import scala.quoted._
// Simple macro
inline def debug(inline expr: Any): Unit = ${debugImpl('expr)}
def debugImpl(expr: Expr[Any])(using Quotes): Expr[Unit] = {
import quotes.reflect._
val exprString = expr.show
'{
println(s"$exprString = ${$expr}")
}
}
// Usage
val x = 42
debug(x + 10) // Prints: "x.+(10) = 52"
// Macro with type parameter
inline def createInstance[T]: T = ${createInstanceImpl[T]}
def createInstanceImpl[T: Type](using Quotes): Expr[T] = {
import quotes.reflect._
val tpe = TypeRepr.of[T]
tpe.classSymbol match {
case Some(cls) =>
// Generate code to construct instance
New(Inferred(tpe)).select(cls.primaryConstructor).appliedToNone.asExprOf[T]
case None =>
report.errorAndAbort(s"Cannot create instance of ${tpe.show}")
}
}
Pattern matching on code:
import scala.quoted._
inline def optimize(inline expr: Int): Int = ${optimizeImpl('expr)}
def optimizeImpl(expr: Expr[Int])(using Quotes): Expr[Int] = {
expr match {
// Optimize x + 0 to x
case '{($x: Int) + 0} => x
// Optimize x * 1 to x
case '{($x: Int) * 1} => x
// Optimize x * 0 to 0
case '{($x: Int) * 0} => '{0}
// No optimization
case _ => expr
}
}
val x = 5
val a = optimize(x + 0) // Optimized to: x
val b = optimize(x * 1) // Optimized to: x
val c = optimize(x * 0) // Optimized to: 0
Generating code:
import scala.quoted._
// Generate a method that sums N integers
inline def sumN(inline n: Int): (Int*) => Int = ${sumNImpl('n)}
def sumNImpl(n: Expr[Int])(using Quotes): Expr[(Int*) => Int] = {
import quotes.reflect._
n.value match {
case Some(count) =>
val params = (1 to count).map(i => s"x$i").toList
// Generate: (x1, x2, ..., xN) => x1 + x2 + ... + xN
'{
(args: Seq[Int]) => args.sum
}
case None =>
report.errorAndAbort("n must be a constant")
}
}
val sum3 = sumN(3)
sum3(1, 2, 3) // 6
Type-Level Programming
Match types:
// Type-level pattern matching
type Elem[X] = X match {
case String => Char
case Array[t] => t
case Iterable[t] => t
case AnyVal => X
}
val charElem: Elem[String] = 'a'
val intElem: Elem[Array[Int]] = 42
val stringElem: Elem[List[String]] = "hello"
// Recursive match types
type LeafElem[X] = X match {
case String => Char
case Array[t] => LeafElem[t]
case Iterable[t] => LeafElem[t]
case AnyVal => X
}
val deepElem: LeafElem[List[Array[String]]] = 'x' // Char
Union and intersection types:
// Union types
def process(value: Int | String): String = value match {
case i: Int => s"Int: $i"
case s: String => s"String: $s"
}
process(42) // "Int: 42"
process("hello") // "String: hello"
// Intersection types
trait Readable {
def read(): String
}
trait Writable {
def write(data: String): Unit
}
def copy(source: Readable, dest: Writable): Unit = {
dest.write(source.read())
}
// Require both traits
def processFile(file: Readable & Writable): Unit = {
val data = file.read()
file.write(data.toUpperCase)
}
Dependent types:
// Path-dependent types
trait Container {
type Element
def add(e: Element): Unit
def get(): Element
}
def transfer(from: Container, to: Container { type Element = from.Element }): Unit = {
to.add(from.get())
}
// Dependent function types
trait Entry {
type Key
def key: Key
}
// Function that returns type dependent on input
val extractKey: (e: Entry) => e.Key = (e: Entry) => e.key
case class StringEntry(key: String) extends Entry {
type Key = String
}
val entry = StringEntry("test")
val key: String = extractKey(entry) // Type refined to String
Type-level arithmetic:
// Church numerals (compile-time numbers)
sealed trait Nat
case object Zero extends Nat
case class Succ[N <: Nat](n: N) extends Nat
type _0 = Zero.type
type _1 = Succ[_0]
type _2 = Succ[_1]
type _3 = Succ[_2]
// Type-level addition
type Add[N <: Nat, M <: Nat] <: Nat = N match {
case Zero.type => M
case Succ[n] => Succ[Add[n, M]]
}
val three: Add[_1, _2] = Succ(Succ(Succ(Zero)))
// Sized collections
case class Vec[N <: Nat, A](values: List[A]) {
def append[M <: Nat](other: Vec[M, A]): Vec[Add[N, M], A] =
Vec(values ++ other.values)
}
val vec1 = Vec[_2, Int](List(1, 2))
val vec2 = Vec[_3, Int](List(3, 4, 5))
val vec5: Vec[Add[_2, _3], Int] = vec1.append(vec2) // Type: Vec[_5, Int]
Derivation with Derives
Automatic type class derivation:
// Define derivable type class
trait Show[T] {
def show(value: T): String
}
object Show {
// Primitive instances
given Show[Int] = (value: Int) => value.toString
given Show[String] = (value: String) => s"\"$value\""
given Show[Boolean] = (value: Boolean) => value.toString
// Derive for case classes
import scala.deriving._
inline def derived[T](using m: Mirror.Of[T]): Show[T] = {
inline m match {
case p: Mirror.ProductOf[T] => productShow(p)
case s: Mirror.SumOf[T] => sumShow(s)
}
}
private def productShow[T](p: Mirror.ProductOf[T]): Show[T] = {
new Show[T] {
def show(value: T): String = {
val values = value.asInstanceOf[Product].productIterator.toList
val labels = constValueTuple[p.MirroredElemLabels].toList
val fields = labels.zip(values).map { case (label, value) =>
s"$label = $value"
}
s"${constValue[p.MirroredLabel]}(${fields.mkString(", ")})"
}
}
}
private def sumShow[T](s: Mirror.SumOf[T]): Show[T] = {
new Show[T] {
def show(value: T): String = value.toString
}
}
}
// Use derives keyword
case class Person(name: String, age: Int) derives Show
val person = Person("Alice", 30)
summon[Show[Person]].show(person) // "Person(name = Alice, age = 30)"
// Multiple derivations
enum Color derives Show, Eq, Ordering {
case Red, Green, Blue
}
// Generic derivation
case class Box[T](value: T) derives Show
given [T: Show]: Show[Box[T]] = Show.derived
Common derivable type classes:
// Eq - equality
trait Eq[T] {
def eqv(x: T, y: T): Boolean
}
case class User(id: Int, name: String) derives Eq
val user1 = User(1, "Alice")
val user2 = User(1, "Alice")
summon[Eq[User]].eqv(user1, user2) // true
// Ordering
case class Score(value: Int) derives Ordering
val scores = List(Score(10), Score(5), Score(20))
scores.sorted // List(Score(5), Score(10), Score(20))
// Encoder/Decoder (JSON libraries)
import io.circe.Codec
case class Event(id: String, timestamp: Long) derives Codec
// Automatically derives JSON encoder and decoder
Compile-Time Operations
Compile-time values:
import scala.compiletime._
// Extract constant values
inline val SIZE = 100
transparent inline def arraySize: Int = constValue[SIZE.type]
val size: 100 = arraySize // Type refined to literal 100
// Const operations
transparent inline def add(inline a: Int, inline b: Int): Int = {
inline val result = constValue[a.type] + constValue[b.type]
result
}
val sum: 5 = add(2, 3) // Type refined to 5
// Error reporting
inline def assertPositive(inline x: Int): Int = {
inline if (constValue[x.type] <= 0) {
error("Value must be positive at compile time")
}
x
}
val good = assertPositive(5) // OK
// val bad = assertPositive(-1) // Compile error
Tuple operations:
import scala.compiletime.ops.int._
// Type-level size
type Size[T <: Tuple] = T match {
case EmptyTuple => 0
case _ *: tail => S[Size[tail]]
}
val size: Size[(Int, String, Boolean)] = 3 // Type: 3
// Type-level concat
type Concat[A <: Tuple, B <: Tuple] <: Tuple = A match {
case EmptyTuple => B
case h *: t => h *: Concat[t, B]
}
val concat: Concat[(Int, String), (Boolean, Double)] = (1, "hi", true, 3.14)
// Type-level reverse
type Reverse[T <: Tuple] <: Tuple = T match {
case EmptyTuple => EmptyTuple
case h *: t => Concat[Reverse[t], h *: EmptyTuple]
}
val reversed: Reverse[(Int, String, Boolean)] = (true, "hi", 1)
Code generation:
import scala.quoted._
// Generate boilerplate code
inline def generateGetters[T]: Unit = ${generateGettersImpl[T]}
def generateGettersImpl[T: Type](using Quotes): Expr[Unit] = {
import quotes.reflect._
val tpe = TypeRepr.of[T]
val fields = tpe.typeSymbol.caseFields
fields.foreach { field =>
println(s"def get${field.name.capitalize}(): ${field.termRef.widenTermRefByName.show}")
}
'{}
}
case class User(name: String, age: Int, email: String)
generateGetters[User]
// Prints at compile time:
// def getName(): String
// def getAge(): Int
// def getEmail(): String
Compile-time reflection:
import scala.quoted._
inline def inspectType[T]: Unit = ${inspectTypeImpl[T]}
def inspectTypeImpl[T: Type](using Quotes): Expr[Unit] = {
import quotes.reflect._
val tpe = TypeRepr.of[T]
println(s"Type: ${tpe.show}")
println(s"Base classes: ${tpe.baseClasses.map(_.name)}")
println(s"Members: ${tpe.typeSymbol.declaredMethods.map(_.name)}")
tpe.typeSymbol.caseFields.foreach { field =>
println(s"Field: ${field.name}: ${field.termRef.widenTermRefByName.show}")
}
'{}
}
case class Person(name: String, age: Int)
inspectType[Person]
// Compile-time output:
// Type: Person
// Base classes: List(Person, Product, Serializable, Equals, Object, Any)
// Members: List(name, age, copy, productElementNames, ...)
// Field: name: String
// Field: age: Int
Metaprogramming Best Practices
When to use metaprogramming:
// Good use cases:
// 1. Eliminating boilerplate
case class User(name: String, age: Int) derives JsonCodec
// 2. Performance optimization
inline def fastSum(xs: Array[Int]): Int = {
var sum = 0
var i = 0
while (i < xs.length) {
sum += xs(i)
i += 1
}
sum
}
// 3. Type-safe APIs
val query = sql"SELECT * FROM users WHERE id = $userId" // Compile-time SQL checking
// 4. Configuration validation
inline val config = validateConfig("config.json") // Compile-time validation
// Bad use cases:
// - Obscuring simple code
// - When runtime reflection would suffice
// - Overly complex type-level programming
Comparison with other languages:
| Feature | Scala 3 | Rust | Haskell | C++ |
|---|---|---|---|---|
| Macros | Quote/splice | Procedural/declarative | Template Haskell | Preprocessor |
| Inline | inline keyword | inline attribute | INLINE pragma | inline keyword |
| Type-level | Match types, unions | Traits, associated types | Type families | Template metaprogramming |
| Derivation | derives keyword | derive macros | deriving clause | Not built-in |
| Safety | Hygienic | Hygienic | Hygienic | Not hygienic |
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
patterns-concurrency-dev- Futures, actors, effects comparison across languagespatterns-serialization-dev- JSON handling, schema validation patternspatterns-metaprogramming-dev- Macros, implicits, type classes vs other languages
References
Official Documentation
- Scala Documentation: https://docs.scala-lang.org/
- Scala 3 Book: https://docs.scala-lang.org/scala3/book/introduction.html
- API Docs: https://www.scala-lang.org/api/current/
Books
- Programming in Scala (Odersky, Spoon, Venners)
- Functional Programming in Scala (Chiusano, Bjarnason)
- Scala with Cats (Underscore)
Community Resources
- Scala Center: https://scala.epfl.ch/
- Scala Users Forum: https://users.scala-lang.org/
- ScalaDex (package index): https://index.scala-lang.org/
Build Tools
- sbt: https://www.scala-sbt.org/
- Mill: https://mill-build.org/
- Maven: https://maven.apache.org/
- Gradle: https://gradle.org/
Testing
- ScalaTest: https://www.scalatest.org/
- Specs2: https://etorreborre.github.io/specs2/
- MUnit: https://scalameta.org/munit/
- ScalaCheck: https://scalacheck.org/
Related Skills
When you need specialized functionality:
- Akka (actors, streams): Use
lang-scala-akka-dev - Cats (FP library): Use
lang-scala-cats-dev - ZIO (effects): Use
lang-scala-zio-dev - Spark (distributed): Use
lang-scala-spark-dev - Play Framework: Use
lang-scala-play-dev - Testing: Use
lang-scala-testing-dev
Summary
This skill covers foundational Scala development:
- Immutability - val vs var, immutable collections
- Pattern matching - case classes, sealed traits, ADTs
- Traits - interfaces with implementation, mixins
- For-comprehensions - monadic composition
- Error handling - Option, Either, Try
- Collections - List, Vector, Set, Map operations
- Higher-order functions - map, filter, fold, composition
- Type system - variance, bounds, type classes
- Common patterns - builder, type classes, cake, ADTs
For specialized topics, route to the appropriate skill from the hierarchy.