Claude-skill-registry lang-scala-dev
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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/lang-scala-dev" ~/.claude/skills/majiayu000-claude-skill-registry-lang-scala-dev && rm -rf "$T"
skills/data/lang-scala-dev/SKILL.mdScala 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 | | Default variable declaration |
| Mutable var | | When mutation is necessary |
| Case class | | Data containers with pattern matching |
| Pattern match | | Destructuring, conditional logic |
| Option | , , | Nullable value handling |
| Either | , , | Error handling with context |
| Try | | Exception handling |
| For-comprehension | | Sequential monadic operations |
| Higher-order fn | | Function composition |
| Trait | | Interface with implementation |
| Object | | Singleton, companion object |
| Implicit (2.x) | | Type class instances |
| Given/Using (3.x) | | Scala 3 implicits |
Skill Routing
| Task | Use This Skill | Rationale |
|---|---|---|
| Akka actors, streams, clustering | | Specialized actor model patterns |
| Cats library, type classes, MTL | | Advanced FP abstractions |
| ZIO effects, fibers, layers | | Effect system patterns |
| Spark jobs, RDDs, DataFrames | | Distributed computing |
| Play web apps, controllers, routes | | Web framework patterns |
| ScalaTest, ScalaCheck, mocking | | Testing strategies |
| Core language features | This skill | Foundational patterns |
Core Language Features
Val vs Var - Immutability
Prefer immutable
over mutable val
: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 |
|---|---|---|
| Value may be absent | No error context |
| Need error details | Custom error type |
| Catching exceptions | 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
Module System
Scala uses packages and objects to organize code. The module system provides flexible import mechanisms, visibility modifiers, and companion objects for namespace management.
Packages
// Package declaration package com.example.myapp // Or nested (less common) package com.example { package myapp { class MyClass } } // Package objects - shared utilities for a package // File: com/example/package.scala package object example { type UserId = Long val DefaultTimeout = 30.seconds def log(message: String): Unit = println(s"[LOG] $message") } // Usage - available to all code in com.example package com.example.myapp class Service { val id: UserId = 123L // From package object log("Service created") // From package object }
Import Patterns
// Basic import import java.time.LocalDate // Import all members import java.time._ // Import multiple specific members import java.time.{LocalDate, LocalTime, ZonedDateTime} // Rename on import (avoid conflicts) import java.util.{List => JList} import scala.collection.immutable.List // Exclude on import import java.util.{Date => _, _} // Import all except Date // Import object members object Utils { def helper(): Unit = ??? } import Utils.helper // Import with alias (Scala 3) import java.time.LocalDate as Date // Import given instances (Scala 3) import MyCodecs.given import MyCodecs.{given JsonEncoder[_]}
Visibility Modifiers
class Example { private val privateField = 1 // This class only protected val protectedField = 2 // This class and subclasses private[this] val instanceOnly = 3 // This instance only private[Example] val sameAsPrivate = 4 // This class (same as private) private[myapp] val packagePrivate = 5 // Package-visible protected[myapp] val packageProtected = 6 val publicField = 7 // Public (default) } // Package-private class private[myapp] class InternalHelper // Sealed for ADTs (visible in same file) sealed trait Result case class Success(value: Int) extends Result case class Failure(error: String) extends Result
Companion Objects
// Class and companion object share private access class User private (val name: String, val age: Int) object User { // Factory method def apply(name: String, age: Int): User = new User(name, age) // Smart constructor with validation def create(name: String, age: Int): Either[String, User] = { if (name.isEmpty) Left("Name cannot be empty") else if (age < 0) Left("Age cannot be negative") else Right(new User(name, age)) } // Extractor for pattern matching def unapply(user: User): Option[(String, Int)] = Some((user.name, user.age)) // Constants val Anonymous: User = new User("Anonymous", 0) } // Usage val user = User("Alice", 30) // Uses apply val result = User.create("Bob", 25) user match { case User(name, age) => println(s"$name is $age") // Uses unapply }
Module Patterns
// Object as module object StringUtils { def capitalize(s: String): String = s.capitalize def reverse(s: String): String = s.reverse // Nested module object Validators { def isEmail(s: String): Boolean = s.contains("@") def isNotEmpty(s: String): Boolean = s.nonEmpty } } // Usage import StringUtils._ import StringUtils.Validators._ capitalize("hello") isEmail("test@example.com")
Scala 3 Exports
// Export delegates to another object class UserRepository { def findById(id: Int): Option[User] = ??? def save(user: User): Unit = ??? def delete(id: Int): Unit = ??? } class UserService(repo: UserRepository) { // Export selected members export repo.{findById, save} // Export all members // export repo._ // Export with rename export repo.{delete as removeUser} def businessLogic(): Unit = { // Uses repo internally } } // Clients can call userService.findById directly val service = new UserService(new UserRepository) service.findById(123) // Delegated to repo
File Organization
// Typical project structure src/main/scala/ ├── com/example/myapp/ │ ├── Main.scala // Entry point │ ├── domain/ │ │ ├── User.scala // User case class + companion │ │ ├── Order.scala // Order case class + companion │ │ └── package.scala // Package object with shared types │ ├── service/ │ │ ├── UserService.scala │ │ └── OrderService.scala │ ├── repository/ │ │ ├── UserRepository.scala │ │ └── OrderRepository.scala │ └── util/ │ └── StringUtils.scala // One public class/trait/object per file (convention) // File name should match primary type name
Import Best Practices
// Standard ordering convention import java.time._ // 1. Java stdlib import scala.concurrent._ // 2. Scala stdlib import cats.effect._ // 3. Third-party libraries import com.example.myapp.domain._ // 4. Project imports // Avoid wildcard imports for large namespaces import java.util._ // Avoid - pollutes namespace // Prefer explicit imports import java.util.{List, Map, Optional} // Better // Exception: well-known small namespaces import cats.syntax.all._ // OK - common in FP code import scala.concurrent.ExecutionContext.Implicits.global // OK - well-known
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 |
|---|---|---|---|---|
| Absence vs presence | No context | Yes | Yes |
| Typed errors | Custom error type | Yes | Yes |
| Exception catching | Throwable | Yes | No |
| Stacked Either/Future | Custom error type | Yes | Yes |
| Effect errors | Throwable | Yes | Yes |
| 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 | 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 |
Serialization
Scala has excellent serialization support with multiple libraries for JSON, XML, and binary formats. This section covers common serialization patterns and library choices.
Library Comparison
| Library | Style | Performance | Type Safety | Derivation |
|---|---|---|---|---|
| circe | FP-first | Good | Excellent | Semi-auto/auto |
| upickle | Simple | Excellent | Good | Automatic |
| play-json | Familiar | Good | Good | Macro-based |
| jsoniter-scala | Performance | Best | Good | Compile-time |
| zio-json | ZIO ecosystem | Excellent | Excellent | Derives |
Circe (Most Popular)
import io.circe._ import io.circe.generic.semiauto._ import io.circe.syntax._ import io.circe.parser._ // Case class case class User(name: String, age: Int, email: Option[String]) // Semi-automatic derivation (recommended) object User { implicit val encoder: Encoder[User] = deriveEncoder[User] implicit val decoder: Decoder[User] = deriveDecoder[User] } // Or combined codec object User { implicit val codec: Codec[User] = deriveCodec[User] } // Encoding val user = User("Alice", 30, Some("alice@example.com")) val json: Json = user.asJson val jsonString: String = user.asJson.noSpaces // {"name":"Alice","age":30,"email":"alice@example.com"} // Decoding val parsed: Either[Error, User] = decode[User](jsonString) parsed match { case Right(user) => println(s"Got user: $user") case Left(error) => println(s"Parse error: $error") } // Custom field names case class ApiResponse( @JsonKey("user_id") userId: Long, @JsonKey("created_at") createdAt: String )
Circe with ADTs
import io.circe._ import io.circe.generic.semiauto._ // Sealed trait hierarchy sealed trait PaymentMethod case class CreditCard(number: String, expiry: String) extends PaymentMethod case class BankTransfer(iban: String) extends PaymentMethod case object Cash extends PaymentMethod object PaymentMethod { implicit val encoder: Encoder[PaymentMethod] = deriveEncoder[PaymentMethod] implicit val decoder: Decoder[PaymentMethod] = deriveDecoder[PaymentMethod] } // Custom discriminator import io.circe.generic.extras.semiauto._ import io.circe.generic.extras.Configuration implicit val config: Configuration = Configuration.default .withDiscriminator("type") .withSnakeCaseMemberNames // Result: {"type":"CreditCard","number":"1234","expiry":"12/25"}
Upickle (Simple & Fast)
import upickle.default._ // Automatic derivation for case classes case class User(name: String, age: Int, email: Option[String]) object User { implicit val rw: ReadWriter[User] = macroRW[User] } // Or inline implicit val userRW: ReadWriter[User] = macroRW // Encoding val user = User("Alice", 30, Some("alice@example.com")) val json: String = write(user) val prettyJson: String = write(user, indent = 2) // Decoding val parsed: User = read[User](json) // Handle errors val result: Either[Throwable, User] = scala.util.Try(read[User](json)).toEither // Custom field names case class ApiUser( @key("user_name") userName: String, @key("user_age") userAge: Int ) object ApiUser { implicit val rw: ReadWriter[ApiUser] = macroRW }
Play JSON
import play.api.libs.json._ // Case class with companion case class User(name: String, age: Int, email: Option[String]) object User { // Automatic format implicit val format: Format[User] = Json.format[User] // Or separate reads/writes implicit val reads: Reads[User] = Json.reads[User] implicit val writes: Writes[User] = Json.writes[User] } // Encoding val user = User("Alice", 30, Some("alice@example.com")) val json: JsValue = Json.toJson(user) val jsonString: String = Json.stringify(json) // Decoding val parsed: JsResult[User] = Json.parse(jsonString).validate[User] parsed match { case JsSuccess(user, _) => println(s"Got user: $user") case JsError(errors) => println(s"Errors: $errors") } // Custom format implicit val customFormat: Format[User] = ( (__ \ "user_name").format[String] and (__ \ "user_age").format[Int] and (__ \ "email_address").formatNullable[String] )(User.apply, unlift(User.unapply))
Jsoniter-Scala (High Performance)
import com.github.plokhotnyuk.jsoniter_scala.core._ import com.github.plokhotnyuk.jsoniter_scala.macros._ case class User(name: String, age: Int, email: Option[String]) // Compile-time codec generation implicit val codec: JsonValueCodec[User] = JsonCodecMaker.make // Encoding val user = User("Alice", 30, Some("alice@example.com")) val bytes: Array[Byte] = writeToArray(user) val json: String = writeToString(user) // Decoding val parsed: User = readFromString[User](json) val fromBytes: User = readFromArray[User](bytes) // Configuration implicit val customCodec: JsonValueCodec[User] = JsonCodecMaker.make( CodecMakerConfig .withFieldNameMapper(JsonCodecMaker.EnforcePascalCase) .withDiscriminatorFieldName(Some("type")) )
ZIO JSON
import zio.json._ case class User(name: String, age: Int, email: Option[String]) object User { implicit val encoder: JsonEncoder[User] = DeriveJsonEncoder.gen[User] implicit val decoder: JsonDecoder[User] = DeriveJsonDecoder.gen[User] // Or combined implicit val codec: JsonCodec[User] = DeriveJsonCodec.gen[User] } // Encoding val user = User("Alice", 30, Some("alice@example.com")) val json: String = user.toJson val prettyJson: String = user.toJsonPretty // Decoding val parsed: Either[String, User] = json.fromJson[User] // With custom field names case class ApiUser( @jsonField("user_name") userName: String, @jsonField("user_age") userAge: Int )
Validation Patterns
import io.circe._ import io.circe.generic.semiauto._ // Custom decoder with validation case class Email private (value: String) object Email { def apply(value: String): Either[String, Email] = if (value.contains("@")) Right(new Email(value)) else Left("Invalid email format") implicit val decoder: Decoder[Email] = Decoder.decodeString.emap(apply) implicit val encoder: Encoder[Email] = Encoder.encodeString.contramap(_.value) } // Refined types integration import eu.timepit.refined._ import eu.timepit.refined.api.Refined import eu.timepit.refined.string._ type ValidEmail = String Refined MatchesRegex["^[\\w.-]+@[\\w.-]+\\.[a-z]{2,}$"] case class User( name: String, email: ValidEmail, age: Int ) // Accumulating validation with cats import cats.data.ValidatedNec import cats.syntax.all._ def validateUser(json: Json): ValidatedNec[String, User] = { ( validateName(json), validateEmail(json), validateAge(json) ).mapN(User.apply) }
XML Serialization
import scala.xml._ case class User(name: String, age: Int) // Manual XML generation def toXml(user: User): Elem = <user> <name>{user.name}</name> <age>{user.age}</age> </user> // Manual XML parsing def fromXml(xml: Elem): User = User( name = (xml \ "name").text, age = (xml \ "age").text.toInt ) // With scala-xml val xml = <users> <user id="1"> <name>Alice</name> <age>30</age> </user> </users> val users = (xml \ "user").map { node => User((node \ "name").text, (node \ "age").text.toInt) }
Binary Formats
// Protocol Buffers with ScalaPB // build.sbt: libraryDependencies += "com.thesamet.scalapb" %% "scalapb-runtime" % "..." // user.proto // message User { // string name = 1; // int32 age = 2; // } // Generated code usage import myapp.proto.user.User val user = User(name = "Alice", age = 30) val bytes: Array[Byte] = user.toByteArray val parsed: User = User.parseFrom(bytes) // Avro with avro4s import com.sksamuel.avro4s._ case class User(name: String, age: Int) val schema = AvroSchema[User] val record = RecordFormat[User].to(User("Alice", 30)) val bytes = AvroOutputStream.binary[User].to(outputStream).write(user).close() // MessagePack with msgpack4s import org.msgpack.core._ // Custom binary protocol trait BinaryCodec[A] { def encode(a: A): Array[Byte] def decode(bytes: Array[Byte]): A }
Streaming JSON
import io.circe.fs2._ import fs2.Stream import cats.effect.IO // Stream JSON array val jsonStream: Stream[IO, Byte] = ??? val users: Stream[IO, User] = jsonStream .through(byteStreamParser) .through(decoder[IO, User]) // Process large files import java.nio.file.Paths import fs2.io.file.Files Files[IO] .readAll(Paths.get("users.json")) .through(byteStreamParser) .through(decoder[IO, User]) .evalMap(user => IO(println(user))) .compile .drain
Best Practices
// 1. Use semi-automatic derivation for control object User { implicit val codec: Codec[User] = deriveCodec[User] // Explicit } // 2. Define codecs in companion objects case class User(name: String, age: Int) object User { implicit val codec: Codec[User] = deriveCodec } // 3. Use Either for parsing results def parseUser(json: String): Either[Error, User] = decode[User](json) // 4. Validate on deserialization implicit val emailDecoder: Decoder[Email] = Decoder.decodeString.emap(Email.apply) // 5. Use consistent naming conventions implicit val config: Configuration = Configuration.default .withSnakeCaseMemberNames // 6. Handle optional fields explicitly case class Config( required: String, optional: Option[String] = None, withDefault: Int = 42 )
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
- Futures, actors, effects comparison across languagespatterns-concurrency-dev
- JSON handling, schema validation patternspatterns-serialization-dev
- Macros, implicits, type classes vs other languagespatterns-metaprogramming-dev
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.