Cheatsheet Scala
Linguagem multi-paradigma na JVM: funcional e orientada a objetos
Scala
Basic and Types
Variables (val and var)
// val (immutable - always prefer): val name = "Anna" val age: Int = 30 // var (mutable - use sparingly): var counter = 0 counter += 1 // Type inference: val x = 42 // Int val pi = 3.14 // Double val s = "hello" // String val b = true // Boolean // Lazy evaluation: lazy val data = loadData() // Multiple assignment (destructuring): val (a, b) = (1, 2) val (x, y, z) = (10, 20, 30)
Use val by default (immutable) — the compiler prevents reassignment and enables optimizations. var only when the state needs to change. lazy val delays evaluation until the first access — ideal for expensive resources. Scala type inference is strong — you rarely need to annotate the type.
Tuples
// Create:
val pair = (1, "one")
val triple = (1, "one", true)
val point = (3.0, 4.0)
// Access by position:
pair._1 // 1
pair._2 // "one"
// Destructuring:
val (x, y) = (10, 20)
val (a, b, c) = (1, 2, 3)
// Multiple return from functions:
def divide(a: Int, b: Int): (Int, Int) =
(a / b, a % b)
val (q, r) = divide(10, 3)
// zip (combine collections):
val pairs = List(1,2,3).zip(List("a","b","c"))
// List((1,"a"), (2,"b"), (3,"c"))Tuples group heterogeneous values without creating a class — ideal for returning multiple values. Access by position (_1, _2) or destructuring on assignment. zip combines two collections into pairs. For data with semantic meaning, prefer case class — tuples do not document what each position represents.
Any, Nothing and Unit
// Any (top type - super of everything): val anything: Any = 42 anything = "text" // OK anything = List(1) // OK // AnyRef (= Java Object): val ref: AnyRef = "string" // AnyVal (primitives): val prim: AnyVal = 42 // Nothing (bottom type - subtype of everything): def fail(msg: String): Nothing = throw new Exception(msg) // Used for unreachable code: def quit(): Nothing = sys.exit(1) // Unit (no meaningful value): def log(msg: String): Unit = println(msg) val u: Unit = () // the only Unit value
Any is the top type (supertype of everything). Nothing is the bottom type (subtype of everything) — functions returning Nothing never complete normally (throw, sys.exit). Unit means the absence of a useful value (like void) but is a real type with the value (). This hierarchy enables full polymorphism.
Data Types
// Numeric: Byte, Short, Int, Long Float, Double // Others: Boolean, Char, String, Unit // Literals: val n = 42 // Int val l = 42L // Long val f = 3.14f // Float val d = 3.14 // Double val c = 'A' // Char val hex = 0xFF // 255 val bin = 0b1010 // 10 // Hierarchy: // Any -> AnyVal (primitives) // Any -> AnyRef (objects = Java Object) // Nothing (bottom type): def fail(msg: String): Nothing = throw new Exception(msg)
In Scala, everything is an object — Int, Boolean have methods (42.toString). Any is the root of the hierarchy (AnyRef = objects, AnyVal = primitives). Nothing is the bottom type (subtype of everything) — used for functions that never return normally. Unit is like void but is a real type with the value ().
Type Conversions
// toString:
42.toString // "42"
s"$42" // "42"
// toInt, toDouble, etc:
"42".toInt // 42
"3.14".toDouble // 3.14
3.14.toInt // 3 (truncates)
42.toDouble // 42.0
'A'.toInt // 65
// Safe conversion (Option):
"42".toIntOption // Some(42)
"abc".toIntOption // None
// BigDecimal (precision):
BigDecimal("3.14159")
BigDecimal(1) / BigDecimal(3)
// Formatting:
"%05d".format(42) // "00042"
"%.2f".format(pi) // "3.14"Conversions in Scala are explicit via toX methods (toInt, toDouble). toInt throws an exception when invalid — use toIntOption for a safe conversion that returns Option. BigDecimal is essential for monetary values (Double has precision errors). format uses printf syntax.
Type Inference and Annotations
// Automatic inference:
val x = 42 // Int
val s = "hello" // String
val l = List(1,2,3) // List[Int]
// When to annotate (required):
def add(a: Int, b: Int): Int = a + b
val f: Int => Int = _ * 2
// Inference in expressions:
val result = if true then 1 else 2 // Int
val msg = if false then "a" else 1 // Any
// Types in collections:
val nums: List[Int] = List(1, 2, 3)
val map: Map[String, Int] = Map("a" -> 1)
// asInstanceOf (cast - avoid):
val n = anything.asInstanceOf[Int]
// isInstanceOf (check):
if anything.isInstanceOf[String] then
println("is a string")Scala type inference is very strong — you rarely need to annotate. Annotations are required in function parameters, public return types (good practice) and when the compiler cannot infer. asInstanceOf is an unchecked cast (avoid — prefer pattern matching). isInstanceOf checks the type at runtime. Prefer match with type patterns.
Strings and Interpolation
// s interpolation:
val name = "World"
s"Hello, $name!" // "Hello, World!"
s"2+2 = ${2 + 2}" // "2+2 = 4"
// f interpolation (formatting):
f"$pi%.2f" // "3.14"
f"$n%05d" // "00042"
// Raw (no escapes):
raw"line1\nline2" // literal \n
// Multi-line:
val text = """
|First
|Second
""".stripMargin
// Operations:
"hello".toUpperCase // "HELLO"
"a,b,c".split(",") // Array("a","b","c")
"abc" * 3 // "abcabcabc"
" abc ".trim // "abc"Scala has three interpolators: s (variable substitution), f (printf-style formatting like %.2f), and raw (escapes not processed). Triple-quoted strings (\"\"\") preserve multiline formatting — stripMargin removes the | prefix. All Java String operations are available.
Option (null safety)
// Option wraps an optional value:
val name: Option[String] = Some("Anna")
val empty: Option[String] = None
// getOrElse (safe default):
name.getOrElse("Anonymous") // "Anna"
empty.getOrElse("Anonymous") // "Anonymous"
// map/flatMap (propagates None):
name.map(_.toUpperCase) // Some("ANA")
empty.map(_.toUpperCase) // None
// Pattern matching:
name match
case Some(n) => println(n)
case None => println("Empty")
// for comprehension:
for n <- name do println(n)
// Convention: never use null
// APIs return OptionOption is the Scala alternative to null — it wraps a value that may or may not exist (Some/None). map/flatMap propagate None automatically (no NullPointerException). getOrElse provides a safe default. The Scala convention is to never use null — well designed APIs return Option.
Operators
// Arithmetic: + - * / % // Comparison: == != > < >= <= // == compares VALUES (not references!) // Logical: && || ! // Bitwise: & | ^ ~ << >> // Range: 1 to 10 // inclusive (1..10) 1 until 10 // exclusive (1..9) 1 to 10 by 2 // step (1,3,5,7,9) // Operators are methods: 1 + 2 == 1.+(2) "abc".reverse == "cba" // Compound assignment: += -= *= /= %=
In Scala, operators are methods — 1 + 2 is sugar for 1.+(2). You can define custom operators in your classes. == compares values (uses equals), not references (unlike Java). Ranges (to = inclusive, until = exclusive, by = step) are used extensively in for loops.
Ranges and Sequences
// Inclusive range:
val r1 = 1 to 10 // Range(1,2,...,10)
// Exclusive range:
val r2 = 1 until 10 // Range(1,2,...,9)
// With step:
val evens = 2 to 20 by 2 // 2,4,6,...,20
val desc = 10 to 1 by -1 // 10,9,...,1
// Convert to List:
val list = (1 to 5).toList // List(1,2,3,4,5)
// Operations on ranges:
(1 to 10).sum // 55
(1 to 10).map(_ * 2)
(1 to 10).filter(_ % 2 == 0)
(1 to 10).foreach(println)
// Char range:
('a' to 'z').toList // List(a,b,...,z)
// Membership check:
(1 to 10).contains(5) // trueRanges are lazy integer sequences — to is inclusive, until exclusive, by sets the step. They work with chars ('a' to 'z'). They support all collection operations (map, filter, sum). They are the idiomatic way to iterate in Scala — replacing indexed loops from imperative languages.
Functions
Defining Functions
// def (method):
def add(a: Int, b: Int): Int = a + b
// Multi-line (Scala 3 indentation):
def factorial(n: Int): Int =
if n <= 1 then 1
else n * factorial(n - 1)
// Default and named args:
def connect(host: String, port: Int = 3306): Unit =
println(s"$host:$port")
connect("localhost")
connect("db", port = 5432)
// Varargs:
def total(nums: Int*): Int = nums.sum
total(1, 2, 3) // 6
// Single expression (inferred type):
def double(n: Int) = n * 2def defines methods (evaluated on every call). The return type can be inferred in simple expressions. Default args eliminate overloads. Varargs (Int*) accepts N arguments. Named args (port = 5432) improve readability. In Scala 3, indentation defines blocks (no braces).
Recursion and @tailrec
// Simple recursion: def factorial(n: Int): BigInt = if n <= 1 then 1 else n * factorial(n - 1) // Tail recursion (optimized): import scala.annotation.tailrec @tailrec def factorialTR(n: Int, acc: BigInt = 1): BigInt = if n <= 1 then acc else factorialTR(n - 1, n * acc) // Fibonacci with tail recursion: @tailrec def fib(n: Int, a: BigInt = 0, b: BigInt = 1): BigInt = if n == 0 then a else fib(n - 1, b, a + b) // LazyList (infinite sequences): lazy val fibs: LazyList[BigInt] = BigInt(0) #:: BigInt(1) #:: fibs.zip(fibs.tail).map(_ + _) fibs.take(10).toList
Recursion replaces imperative loops in FP. @tailrec guarantees the compiler optimizes into a loop (no stack overflow) — if the function is not tail-recursive, it is a compile error. The accumulator (acc) carries the partial result. LazyList enables infinite sequences — it only computes the needed elements (take(10) does not compute the rest).
Inline and Transparent Functions
// inline (removes overhead at compile time): inline def max(inline a: Int, inline b: Int): Int = if a > b then a else b // No call overhead: val x = max(10, 20) // compiled the a direct if // transparent inline (precise type): transparent inline def get(id: Int): Any = inline if id == 1 then "string" else 42 val s: String = get(1) // OK! type = String val n: Int = get(2) // OK! type = Int // inline given: inline given intOrd: Ordering[Int] = Ordering.Int // compiletime operations: import scala.compiletime.* inline def size[T]: Int = constValue[Tuple.Size[T]]
inline (Scala 3) removes abstraction overhead — the code is expanded at the call site at compile time. transparent inline lets the compiler infer the precise type (not the declared type). inline if is resolved at compile time. Essential for high-performance libraries and type-level programming. Zero runtime cost for abstractions.
Lambdas and Anonymous Functions
// Full syntax: val add = (a: Int, b: Int) => a + b // Type inference: val double = (x: Int) => x * 2 val double2: Int => Int = _ * 2 // Placeholder syntax (_): List(1,2,3).map(_ * 2) List(1,2,3).filter(_ > 1) List(1,2,3).reduce(_ + _) // No parameters: val now = () => System.currentTimeMillis() // As an argument: def applyTo(f: Int => Int, x: Int): Int = f(x) applyTo(_ * 3, 5) // 15 // Multiple underscores: // _ + _ == (a, b) => a + b List(1,2,3).reduce(_ + _) // 6
Lambdas are first-class values in Scala. The underscore (_) is a placeholder for the parameter — _ * 2 is equivalent to x => x * 2. Each _ represents a different parameter (_ + _ = (a,b) => a + b). The placeholder syntax makes collection operations extremely concise.
Functions the Values
// Assign to a val:
val double: Int => Int = _ * 2
double(5) // 10
// List of functions:
val ops: List[Int => Int] = List(_ + 1, _ * 2, _ - 3)
ops.map(f => f(10)) // List(11, 20, 7)
// Return a function (closure):
def multiplier(factor: Int): Int => Int =
_ * factor
val triple = multiplier(3)
triple(5) // 15
// Closure with state:
def counter() =
var n = 0
() => { n += 1; n }
// Function traits:
// Function0[R], Function1[T,R], Function2[T1,T2,R]
val f: Function1[Int, Int] = _ * 2
val g: Int => Int = _ * 2 // equivalentFunctions are first-class values — they can be stored, passed, returned and placed in collections. Int => Int is sugar for Function1[Int, Int]. Closures capture variables from the environment (counter keeps state). Function factories (multiplier) create parameterized behavior without classes.
Higher-Order Functions
val nums = List(1, 2, 3, 4, 5)
// map (transform):
nums.map(_ * 2) // List(2,4,6,8,10)
// filter (select):
nums.filter(_ % 2 == 0) // List(2,4)
// reduce / fold (aggregate):
nums.reduce(_ + _) // 15
nums.fold(0)(_ + _) // 15
nums.foldLeft(0)(_ + _) // 15
// flatMap (map and flatten):
List("ab","cd").flatMap(_.toList)
// List(a, b, c, d)
// Function composition:
val f = (x: Int) => x * 2
val g = (x: Int) => x + 1
val h = f compose g // f(g(x))
val k = f andThen g // g(f(x))
// foreach (side effect):
nums.foreach(println)HOFs take or return functions — the basis of functional programming. map transforms, filter selects, fold/reduce aggregate. flatMap maps and flattens. compose/andThen compose functions (f∘g vs g∘f). These operations chain into pipelines without mutable variables.
Methods vs Functions
// Method (def):
def method(x: Int): Int = x * 2
// Function (val):
val func: Int => Int = x => x * 2
// Eta expansion (method -> function):
val f = method _ // Int => Int
val g = method // Scala 3: auto-converts
// Differences:
// - def is evaluated on each call
// - val is evaluated once
// - def can have type params
// - val is an object (Function1)
// By-name parameters:
def repeat(n: Int)(block: => Unit): Unit =
(1 to n).foreach(_ => block)
repeat(3) {
println("Hello") // executed 3x
}def is a method (evaluated on each call, can have type parameters, is not a value). A val with a function type is a Function1 object (evaluated once, can be passed around). By-name parameters (=> Unit) delay argument evaluation — the block is re-evaluated on each use (essential for DSLs and custom control flow).
Currying and Partial Application
// Currying (multiple param lists):
def add(a: Int)(b: Int): Int = a + b
add(3)(4) // 7
// Partial application:
val add3 = add(3) _
add3(4) // 7
add3(10) // 13
// Multiple parameters:
def format(prefix: String)(value: Int)(suffix: String) =
s"$prefix$value$suffix"
val euros = format("€")(_: Int)("")
euros(42) // "€42"
// fold with currying:
val nums = List(1, 2, 3, 4, 5)
nums.foldLeft(0)(_ + _) // 15
nums.foldLeft("")(_ + _) // "12345"
// Pipe operator (Scala 3):
val result = 5
|> (x => x * 2)
|> (x => x + 1) // 11Currying transforms a function of N parameters into N functions of 1 parameter — enabling partial application (fixing arguments). Multiple parameter lists are idiomatic (foldLeft(0)(_ + _)). The pipe operator |> (Scala 3) chains transformations left to right — more readable than nested composition.
PartialFunction
// PartialFunction (defined for a subset):
val pf: PartialFunction[Int, String] =
case 1 => "one"
case 2 => "two"
pf(1) // "one"
pf.isDefinedAt(3) // false
pf(3) // MatchError!
// collect uses PartialFunction:
List(1, 2, 3, 4).collect {
case n if n % 2 == 0 => n * 10
}
// List(20, 40)
// orElse (compose partials):
val even: PartialFunction[Int, String] =
case n if n % 2 == 0 => "even"
val odd: PartialFunction[Int, String] =
case n if n % 2 == 1 => "odd"
val all = even orElse odd
// andThen (transform result):
val loud = pf andThen (_ + "!")PartialFunction is only defined for certain inputs — used in pattern matching and collect. isDefinedAt checks whether the input is valid. orElse composes partials (fallback). andThen transforms the result. Essential in actors (Akka) and routing. Unlike Function1: it may not be defined for all inputs of the type.
Classes e Traits
Classes
class Person(val name: String, var age: Int):
def greeting: String = s"Hello, I am $name"
def growOlder(): Unit = age += 1
// Usage:
val p = Person("Anna", 30)
p.name // "Anna" (val = getter)
p.age = 31 // OK (var = setter)
p.greeting
// Auxiliary constructor:
class Point(val x: Double, val y: Double):
def this() = this(0, 0)
def distance: Double = math.sqrt(x*x + y*y)
// Private constructor:
class Config private (val host: String):
def this() = this("localhost")
// Override:
override def toString = s"Point($x, $y)"Constructor parameters with val/var automatically become fields (no boilerplate). val = getter only (immutable), var = getter + setter. Auxiliary constructors (def this) must call the primary one. A private constructor forces the use of factory methods. Scala 3 uses indentation instead of braces.
Generics
// Generic class:
class Box[T](val content: T):
def map[U](f: T => U): Box[U] =
Box(f(content))
val c = Box(42)
val s = c.map(_.toString) // Box[String]
// Bounds:
def maximum[T <: Comparable[T]](a: T, b: T): T =
if a.compareTo(b) > 0 then a else b
// Context bounds (type class):
def sortList[T: Ordering](list: List[T]): List[T] =
list.sorted
// Variance:
class Covariant[+T] // Producer
class Contravariant[-T] // Consumer
class Invariant[T]
// Wildcards:
def sizeOf(l: List[_]): Int = l.sizeGenerics in Scala are more expressive than Java: +T (covariant — Box[Dog] is a subtype of Box[Animal]), -T (contravariant), T (invariant). Upper bound (<:) requires a subtype, context bound (T: Ordering) requires a given instance. Variance annotations are checked by the compiler.
Opaque Types and Value Classes
// Opaque type (type-safe, zero overhead):
opaque type UserId = Int
object UserId:
def apply(id: Int): UserId = id
extension (id: UserId)
def value: Int = id
opaque type Email = String
object Email:
def from(s: String): Option[Email] =
if s.contains("@") then Some(s) else None
// Usage (type-safe):
def fetch(id: UserId): User = ???
val uid = UserId(42)
fetch(uid) // OK
// fetch(42) // ERROR! Int != UserId
// Value class (Scala 2, still valid):
case class Meters(val value: Double) extends AnyVal:
def +(o: Meters) = Meters(value + o.value)
val d = Meters(100) + Meters(50)Opaque types (Scala 3) create distinct types with no runtime overhead — UserId is Int at runtime but a separate type at compile time. They prevent mixing values by mistake (UserId vs OrderId). Value classes (extends AnyVal) are the Scala 2 alternative. Both eliminate the "primitive obsession" class of bugs at zero cost.
Case Classes
case class User(name: String, age: Int, email: String)
// Automatic: equals, hashCode, toString, copy
val u1 = User("Anna", 30, "a@b.c")
val u2 = u1.copy(age = 31)
// Pattern matching:
u1 match
case User(name, age, _) => s"$name: $age"
// Destructuring:
val User(n, i, _) = u1
// Sealed hierarchy (ADT):
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
case object Point extends Shape
def area(s: Shape): Double = s match
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
case Point => 0case class automatically generates equals, hashCode, toString, copy and unapply (for pattern matching). copy creates a copy with changed fields — essential for immutable updates. sealed trait + case classes form ADTs — the compiler checks exhaustiveness in match.
Type Classes
// Define type class:
trait JsonEncoder[T]:
def encode(value: T): String
// Instances (given):
given JsonEncoder[Int] with
def encode(value: Int) = value.toString
given JsonEncoder[String] with
def encode(value: String) = s"\"$value\""
// Derived instance (recursive):
given [T](using enc: JsonEncoder[T]): JsonEncoder[List[T]] with
def encode(value: List[T]) =
value.map(enc.encode).mkString("[", ",", "]")
// Use with context bound:
def toJson[T: JsonEncoder](value: T): String =
summon[JsonEncoder[T]].encode(value)
toJson(42) // "42"
toJson(List(1,2,3)) // "[1,2,3]"Type classes enable ad-hoc polymorphism without inheritance — define behavior (trait), create instances for specific types (given), and use with context bounds. The instance for List[T] is derived automatically if an instance for T exists. summon[T] gets the given instance in scope. Central pattern in Cats and Circe.
Traits and Mixins
// Trait (interface + implementation):
trait Swimmer:
def swim(): String = "Swimming"
trait Flyer:
def fly(): String
// Implement multiple:
class Duck extends Swimmer, Flyer:
def fly(): String = "Flying (low)"
// Trait with state:
trait Logger:
var logs: List[String] = Nil
def log(msg: String): Unit =
logs = msg :: logs
// Stackable traits:
trait Base:
def process(s: String): String = s
trait Upper extends Base:
override def process(s: String) =
super.process(s).toUpperCase
trait Exclaim extends Base:
override def process(s: String) =
super.process(s) + "!"Traits are more powerful than interfaces — they can have implementation, state and be composed via mixins. Stackable traits allow composing behaviors in a chain (Upper + Exclaim). Mixin order matters — super calls the previous trait in the chain (linearization). A class can extend multiple traits (no diamond problem).
Objects and Companions
// Singleton object:
object AppConfig:
val version = "1.0"
def start(): Unit = println("Started")
AppConfig.start()
// Companion object:
class User private (val name: String, val age: Int)
object User:
// Factory method:
def apply(name: String, age: Int): User =
new User(name, age)
// Extractor:
def unapply(u: User): Option[(String, Int)] =
Some((u.name, u.age))
val anonymous = new User("Anonymous", 0)
// apply allows:
val u = User("Anna", 30) // without new
// main method (Scala 3):
@main def app(): Unit =
println("Hello Scala 3")object defines singletons (one instance per JVM). A companion object shares its name with a class and has mutual access to private members. apply() is the conventional factory method — it allows User("Anna") without new. unapply() enables pattern matching and destructuring. @main (Scala 3) defines entry points without an object with def main.
Inheritance and Abstract Classes
// Abstract class:
abstract class Animal(val name: String):
def sound(): String
def description: String = s"$name makes ${sound()}"
class Dog(name: String) extends Animal(name):
def sound(): String = "Woof!"
class Cat(name: String) extends Animal(name):
def sound(): String = "Meow!"
// Final and sealed:
final class Immutable // cannot be extended
sealed trait Expr // children in the same file
// Object (singleton):
object Database:
private var conn: Connection = null
def connect(): Unit = ???
// Companion object:
class User(val name: String)
object User:
def apply(name: String) = new User(name)
def anonymous = new User("Anonymous")Scala supports single inheritance (extends) with abstract class for shared code. sealed restricts subclasses to the same file — essential for exhaustive pattern matching. object defines singletons. A companion object (same name the the class) defines factory methods (apply allows User("Anna") without new). ??? throws NotImplementedError.
Enum (Scala 3)
// Simple enum:
enum Color:
case Red, Green, Blue
// Enum with parameters:
enum Planet(val mass: Double, val radius: Double):
case Mercury extends Planet(3.303e+23, 2.4397e6)
case Earth extends Planet(5.976e+24, 6.37814e6)
def gravity: Double =
6.67300E-11 * mass / (radius * radius)
// Enum ADT (with case classes):
enum Shape:
case Circle(r: Double)
case Rectangle(w: Double, h: Double)
// Pattern matching:
def describe(c: Color): String = c match
case Color.Red => "hot"
case Color.Green => "neutral"
case Color.Blue => "cold"
// values and valueOf:
Color.values // Array(Red, Green, Blue)
Color.valueOf("Green")enum (Scala 3) is sugar for sealed trait + case object/case class. It supports parameters, methods and exhaustive pattern matching. values lists all cases, valueOf converts a string. Enums with parameters (like Planet) replace verbose Scala 2 patterns. Ideal for finite sets of variants.
Collections
List and Seq
// Create: val list = List(1, 2, 3, 4, 5) val empty = List.empty[Int] val range = (1 to 10).toList val fill = List.fill(5)(0) // Access: list.head // 1 list.tail // List(2,3,4,5) list.last // 5 list.init // List(1,2,3,4) list.length // 5 // Building: 0 :: list // prepend O(1) list :+ 6 // append O(n) list ++ List(6, 7) // concat // Pattern matching: list match case Nil => "empty" case h :: t => s"head=$h" // Vector (O(1) access): val v = Vector(1, 2, 3)
List is an immutable linked list — prepend (::) is O(1), index access is O(n). Ideal for recursion and when you always add at the head. Vector offers O(1) access to any index (trie tree). head/tail decompose for pattern matching. All collections are immutable by default.
Sorting and Transformation
val people = List(
("Anna", 30), ("Ray", 25), ("Eve", 35)
)
// Sort:
people.sortBy(_._2) // by age (asc)
people.sortBy(_._1) // by name
people.sortWith((a, b) => a._2 > b._2) // desc
// distinct:
List(1,2,2,3,3,3).distinct // List(1,2,3)
// sliding / grouped:
List(1,2,3,4,5).sliding(3).toList
// List(List(1,2,3), List(2,3,4), List(3,4,5))
List(1,2,3,4,5).grouped(2).toList
// List(List(1,2), List(3,4), List(5))
// flatten / transpose:
List(List(1,2), List(3,4)).flatten
List(List(1,2), List(3,4)).transpose
// scanLeft (fold with intermediates):
List(1,2,3).scanLeft(0)(_ + _) // List(0,1,3,6)sortBy sorts by an extracted key (ascending); sortWith uses a custom comparator. sliding creates overlapping windows (moving averages), grouped splits into blocks (pagination). flatten flattens nested collections, transpose swaps rows and columns. scanLeft returns all the intermediate results of the fold.
groupBy and Transformations
val words = List("cat", "dog", "cow", "duck")
// groupBy:
words.groupBy(_.head)
// Map(c -> List(cat, cow), d -> List(dog, duck))
// Count frequencies:
val text = "banana"
text.groupBy(identity).map((c, cs) => (c, cs.length))
// Map(b -> 1, a -> 3, n -> 2)
// mapValues (transform values):
val scores = Map("Anna" -> 95, "Ray" -> 87)
scores.map((k, v) => (k, v + 5))
// flatMap with Option:
val nums = List("1", "abc", "3")
nums.flatMap(_.toIntOption) // List(1, 3)
// unzip:
val pairs = List((1,"a"), (2,"b"), (3,"c"))
val (ints, strs) = pairs.unzip
// mkString:
List(1,2,3).mkString(", ") // "1, 2, 3"groupBy groups by key returning Map[K, List[V]]. flatMap with toIntOption filters and converts in one operation (ignores None). unzip splits a list of tuples into two lists. mkString joins elements with a separator. These operations combine for complex data transformations in a declarative way.
Map and Set
// Map:
val scores = Map("Anna" -> 95, "Ray" -> 87)
// Access:
scores("Anna") // 95 (throws if missing)
scores.getOrElse("Eve", 0) // 0
scores.get("Anna") // Some(95)
scores.contains("Anna") // true
// Modify (immutable = new Map):
val added = scores + ("Eve" -> 100)
val removed = scores - "Ray"
val merged = scores ++ Map("New" -> 50)
// Iterate:
for (name, score) <- scores do
println(s"$name: $score")
// Set:
val colors = Set("red", "green", "blue")
colors + "yellow"
colors.contains("red") // true
Set(1,2,3) & Set(2,3,4) // intersection
Set(1,2,3) | Set(3,4,5) // unionAn immutable Map returns a new Map on every operation (+, -, ++) — the original never changes. get() returns Option (safe), apply() throws an exception if the key does not exist. Set guarantees uniqueness and supports mathematical operations (& intersection, | union). Destructuring (name, score) <- scores is idiomatic.
LazyList and Views
// LazyList (lazy evaluation): lazy val naturals: LazyList[Int] = 1 #:: naturals.map(_ + 1) naturals.take(10).toList // List(1..10) // Infinite Fibonacci: lazy val fibs: LazyList[BigInt] = BigInt(0) #:: BigInt(1) #:: fibs.zip(fibs.tail).map(_ + _) fibs.take(10).toList // View (no intermediate collections): val result = (1 to 1000000).view .filter(_ % 2 == 0) .map(_ * _) .take(5) .toList // Iterator (single-use): val iter = Iterator.from(1) iter.take(5).toList // List(1,2,3,4,5)
LazyList evaluates elements on demand — it allows infinite sequences without infinite memory. view avoids intermediate collections in operation chains (processes element by element). Iterator is single-use (cannot re-iterate). Use view for performance on large collections with multiple transformations. #:: is the lazy constructor (like :: for List).
zip, sliding and Combinators
// zip (combine into pairs):
val a = List(1, 2, 3)
val b = List("a", "b", "c")
a.zip(b) // List((1,"a"), (2,"b"), (3,"c"))
// zipWithIndex:
a.zipWithIndex // List((1,0), (2,1), (3,2))
// zipAll (with default):
List(1,2).zipAll(List("a","b","c"), 0, "?")
// sliding (windows):
List(1,2,3,4,5).sliding(3).toList
// List(List(1,2,3), List(2,3,4), List(3,4,5))
// grouped (blocks):
List(1,2,3,4,5).grouped(2).toList
// List(List(1,2), List(3,4), List(5))
// combinations / permutations:
List(1,2,3).combinations(2).toList
List(1,2,3).permutations.toList
// tabulate:
List.tabulate(5)(n => n * n) // List(0,1,4,9,16)zip combines two collections into pairs (stops at the shortest). zipWithIndex adds indices. sliding creates overlapping windows (moving averages), grouped splits into fixed blocks. combinations/permutations generate mathematical combinations. tabulate creates a list from a function of the index. All return lazy iterators.
Functional Operations
val nums = List(1, 2, 3, 4, 5, 6, 7, 8)
// Transformation:
nums.map(_ * 2)
nums.filter(_ % 2 == 0)
nums.flatMap(n => List(n, n * 10))
nums.collect { case n if n > 5 => n * 2 }
// Aggregation:
nums.reduce(_ + _) // 36
nums.fold(0)(_ + _) // 36
nums.sum // 36
nums.count(_ % 2 == 0) // 4
// Search:
nums.find(_ > 5) // Some(6)
nums.exists(_ > 7) // true
nums.forall(_ > 0) // true
// Grouping:
nums.groupBy(_ % 2)
nums.partition(_ % 2 == 0)
nums.zip(nums.map(_ * 2))collect combines filter + map with pattern matching. foldLeft accumulates from the left (order matters). find returns Option. groupBy groups by key (returns Map). partition splits into two groups (tuple of collections). These operations chain into functional pipelines without mutable variables.
Array and Mutable Collections
// Array (mutable, fixed size):
val arr = Array(1, 2, 3, 4, 5)
arr(0) = 10 // mutable!
arr.length // 5
// ArrayBuffer (variable size):
import scala.collection.mutable.ArrayBuffer
val buf = ArrayBuffer(1, 2, 3)
buf += 4
buf ++= List(5, 6)
buf.remove(0)
// Mutable HashMap:
import scala.collection.mutable
val map = mutable.Map("a" -> 1)
map("b") = 2
map += ("c" -> 3)
map -= "a"
// Convert:
val immutable = buf.toList
val mutableBuf = list.to(ArrayBuffer)
// Convention: immutable by defaultArray is the Java array with fixed size and in-place mutation. ArrayBuffer grows dynamically (like ArrayList). Mutable collections (mutable.Map, mutable.Set) modify in-place — more performant but less safe in concurrent code. The convention is to use immutables by default and mutables only in measured hot paths.
Option, Either, Try
// Option (missing value):
val opt: Option[Int] = Some(42)
opt.map(_ * 2) // Some(84)
opt.getOrElse(0) // 42
opt.filter(_ > 40) // Some(42)
// Either (explicit error):
type Result[A] = Either[String, A]
def parse(s: String): Result[Int] =
s.toIntOption match
case Some(n) => Right(n)
case None => Left(s"Invalid: $s")
// For comprehension:
val total = for
a <- parse("10")
b <- parse("20")
yield a + b // Right(30)
// Try (caught exception):
import scala.util.Try
Try("42".toInt) // Success(42)
Try("abc".toInt) // Failure(...)Three types to represent failure: Option (missing value, no error info), Either (explicit error — Left=error, Right=success), Try (caught exception). Either is preferred for domain errors because it documents the error type. All work in for comprehensions — None/Left/Failure short-circuits.
fold, reduce and scan
val nums = List(1, 2, 3, 4, 5)
// reduce (without seed):
nums.reduce(_ + _) // 15
nums.reduce(_ * _) // 120
nums.reduceLeft(_ - _) // (((1-2)-3)-4)-5 = -13
// fold (with seed):
nums.fold(0)(_ + _) // 15
nums.fold("")(_ + _) // "12345"
nums.fold(1)(_ * _) // 120
// foldLeft vs foldRight:
nums.foldLeft(0)(_ - _) // -15
nums.foldRight(0)(_ - _) // 3
// scan (intermediate results):
nums.scanLeft(0)(_ + _) // List(0,1,3,6,10,15)
// Applications:
val max = nums.reduce(_ max _)
val concat = nums.map(_.toString).reduce(_ + _)
val freq = nums.foldLeft(Map.empty[Int, Int]) { (m, n) =>
m.updated(n, m.getOrElse(n, 0) + 1)
}reduce aggregates without an initial value (fails on an empty list). fold accepts a seed (safe on empty ones). foldLeft processes from the left (efficient on List), foldRight from the right (order matters for non-commutative operations). scanLeft returns all intermediate steps. fold is the most general operation — map/filter can be expressed with fold.
Advanced
Futures and Concurrency
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
// Create Future:
val fut = Future {
Thread.sleep(1000)
42
}
// Transform:
val result = fut
.map(_ * 2)
.filter(_ > 50)
.recover { case _ => 0 }
// For comprehension:
val total = for
a <- Future(10)
b <- Future(20)
yield a + b // Future(30)
// Sequence of Futures:
val futures = List(Future(1), Future(2), Future(3))
val all = Future.sequence(futures)
// Await (blocking - avoid):
import scala.concurrent.Await
import scala.concurrent.duration._
val value = Await.result(fut, 5.seconds)Future represents a value computed asynchronously. map/filter/recover transform without blocking. For comprehension with Futures runs in sequence. Future.sequence converts List[Future[A]] into Future[List[A]]. Await.result blocks the thread — avoid in production. recover handles failures (like catch).
SBT and Project
// build.sbt: name := "my-project" version := "1.0" scalaVersion := "3.3.1" libraryDependencies ++= Seq( "org.typelevel" %% "cats-core" % "2.10.0", "org.scalatest" %% "scalatest" % "3.2.17" % Test ) // SBT commands: // sbt compile // sbt run // sbt test // sbt console (REPL) // sbt assembly (fat jar) // Structure: // src/main/scala/ // src/test/scala/ // project/build.properties // scala-cli (quick scripts): // scala-cli run app.scala // Ammonite (advanced REPL): // amm
SBT (Scala Build Tool) is the standard build — it manages compilation, dependencies, tests and publishing. %% automatically adds the Scala version to the artifact. % Test marks test-only dependencies. scala-cli is the modern tool for quick scripts (single-file). sbt assembly generates a fat JAR for deployment.
Testing with ScalaTest
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class StackSpec extends AnyFlatSpec with Matchers:
"A Stack" should "pop values in LIFO order" in {
val stack = scala.collection.mutable.Stack[Int]()
stack.push(1)
stack.push(2)
stack.pop() should be (2)
stack.pop() should be (1)
}
it should "throw when empty" in {
val stack = scala.collection.mutable.Stack[Int]()
an [NoSuchElementException] should be thrownBy {
stack.pop()
}
}
// Property-based testing:
import org.scalatestplus.scalacheck._
"Sorting" should "preserve length" in {
forAll { (xs: List[Int]) =>
xs.sorted.length shouldEqual xs.length
}
}ScalaTest is the standard testing framework — AnyFlatSpec with Matchers provides a readable DSL (should be, shouldEqual). It supports multiple styles (FlatSpec, FunSuite, WordSpec). ScalaCheck adds property-based testing (generates random inputs and verifies invariants). Tests run with sbt test.
Implicits and Given/Using
// Scala 3 - Given/Using:
trait Show[T]:
def show(value: T): String
given intShow: Show[Int] with
def show(v: Int) = s"Int($v)"
given listShow[T](using s: Show[T]): Show[List[T]] with
def show(v: List[T]) =
v.map(s.show).mkString("[", ",", "]")
def render[T](value: T)(using s: Show[T]): String =
s.show(value)
render(42) // "Int(42)"
render(List(1,2,3)) // "[Int(1),Int(2),Int(3)]"
// summon (get instance):
val s = summon[Show[Int]]
// Extension methods with given:
extension [T](value: T)(using s: Show[T])
def showStr: String = s.show(value)given/using is the implicit resolution mechanism of Scala 3 — the compiler automatically looks for given instances in scope. It enables type classes without manually passing dependencies. summon[T] gets the instance explicitly. Extension methods with given add conditional methods. It replaces the confusing implicit of Scala 2.
Cats and FP Libraries
import cats._
import cats.implicits._
// Applicative (combine N effects):
(Option(1), Option(2)).mapN(_ + _) // Some(3)
(Option(1), None).mapN(_ + _) // None
// Traverse:
List("1","2","3").traverse(_.toIntOption)
// Some(List(1,2,3))
// Validated (accumulate errors):
import cats.data.ValidatedNel
type Errors[A] = ValidatedNel[String, A]
def validupToge(n: Int): Errors[Int] =
if n >= 0 then n.validNel
else "Invalid age".invalidNel
// IO (Cats Effect):
import cats.effect.IO
val program: IO[Unit] = for
_ <- IO.println("Name?")
name <- IO.readLine
_ <- IO.println(s"Hello, $name!")
yield ()Cats provides type classes (Functor, Monad, Applicative) and universal operations. mapN combines N Options/Eithers without for comprehension. traverse converts List[Option[A]] into Option[List[A]]. Validated accumulates all errors (Either short-circuits on the first). Cats Effect IO manages side effects in a pure and testable way.
Monads and For Comprehension
// Option the monad:
def findUser(id: Int): Option[User] = ???
def findEmail(u: User): Option[String] = ???
val email = for
user <- findUser(1)
email <- findEmail(user)
yield email // Option[String]
// Either the monad:
type Result[A] = Either[String, A]
def validate(s: String): Result[Int] =
s.toIntOption.toRight(s"Invalid: $s")
val total: Result[Int] = for
a <- validate("10")
b <- validate("20")
yield a + b
// Future the monad:
val result: Future[String] = for
user <- fetchUser(1)
posts <- fetchPosts(user.id)
yield s"${user.name}: ${posts.size} posts"A Monad is any type with map and flatMap — Option, Either, Future, List, Try. For comprehension is sugar for chained flatMap/map (each <- is a flatMap, yield is map). If any step returns None/Left/Failure, the following ones are skipped (short-circuit).
Extension Methods
// Simple extension:
extension (s: String)
def shout: String = s.toUpperCase + "!"
def words: List[String] = s.split(" ").toList
def truncate(n: Int): String =
if s.length <= n then s else s.take(n) + "..."
"hello world".shout // "HELLO WORLD!"
"a b c".words // List("a","b","c")
"hello world".truncate(5) // "hello..."
// Generic extension:
extension [T](list: List[T])
def second: Option[T] = list.lift(1)
def pairs: List[(T, T)] =
list.sliding(2).collect { case List(a, b) => (a, b) }.toList
List(1,2,3).second // Some(2)
// Extension with given:
extension [T](value: T)(using s: Show[T])
def showStr: String = s.show(value)Extension methods (Scala 3) add methods to existing types without wrapper classes or inheritance. They can be generic ([T]) and conditional (using). They replace implicit classes of Scala 2 with clearer syntax. Ideal for utilities on String, List and domain types. The method is resolved statically — zero runtime overhead.
Advanced Type System
// Union types (Scala 3):
def process(x: Int | String): String = x match
case n: Int => s"Number: $n"
case s: String => s"Text: $s"
// Intersection types:
trait A { def a(): Unit }
trait B { def b(): Unit }
def useBoth(x: A & B): Unit = { x.a(); x.b() }
// Opaque types:
opaque type UserId = Int
object UserId:
def apply(id: Int): UserId = id
// Match types:
type Elem[X] = X match
case String => Char
case List[t] => t
case Array[t] => t
type A = Elem[String] // Char
type B = Elem[List[Int]] // IntScala 3 introduces union types (A | B — replaces overloads), intersection types (A & B — combines traits), and opaque types (type-safe with no runtime overhead). Match types compute types at compile time. inline eliminates abstraction overhead. These features make the type system Turing-complete — the most expressive among mainstream languages.
Error Handling Patterns
// 1. Option (absence without error):
def find(id: Int): Option[User] = ???
// 2. Either (domain error):
def parse(s: String): Either[ParseError, Int] = ???
// 3. Try (wrapping Java exceptions):
import scala.util.Try
def readFile(path: String): Try[String] =
Try(scala.io.Source.fromFile(path).mkString)
// 4. Validated (accumulate errors):
import cats.data.ValidatedNel
def validateForm(name: String, age: Int)
: ValidatedNel[String, User] =
(validateName(name), validupToge(age)).mapN(User.apply)
// 5. Combined Either:
type AppError = String
type Result[A] = Either[AppError, A]
def process(input: String): Result[Data] =
for
parsed <- parse(input)
validated <- validate(parsed)
saved <- save(validated)
yield savedScala offers multiple levels of error handling: Option (simple absence), Either (typed domain error), Try (Java exceptions), Validated (accumulate multiple errors). The choice depends on the context: public APIs use Either, Java wrapping uses Try, forms use Validated. All compose in for comprehensions.
Flow Control
If / Else (Expression)
// If is an expression (returns a value):
val result = if age >= 18 then
"Adult"
else
"Minor"
// If/else if/else:
val msg = if grade >= 18 then "Excellent"
else if grade >= 10 then "Passed"
else "Failed"
// Old syntax (still valid):
val x = if (cond) a else b
// Side effect (Unit):
if x > 0 then
println("Positive")
// Different types -> Any:
val v: Any = if c then 1 else "a"
// Replaces the ternary of other languages:
val status = if active then "online" else "offline"In Scala, if is an expression that returns a value — it replaces the ternary operator. Scala 3 introduces the then/else syntax without parentheses. If the branches have different types, the result is Any. For pure side effects, the result is Unit. There is no ? : ternary operator — the if expression fills that role.
Advanced Pattern Matching
// Nested patterns:
data match
case Some(List(x, y, _*)) => s"$x, $y"
case Some(Nil) => "empty list"
case None => "nothing"
// Type patterns:
def process(x: Any): String = x match
case s: String => s"String: $s"
case n: Int => s"Int: $n"
case l: List[_] => s"List of ${l.size}"
case (a, b) => s"Pair: $a, $b"
case _ => "other"
// Variable binding (@):
case list @ List(_, _, _) =>
s"List of 3: $list"
// Sealed + exhaustive:
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends ShapeNested patterns (Some(List(x, y, _*))) decompose complex structures in one line — _* captures the rest. Variable binding (@) names the whole value while matching its structure. sealed trait + pattern matching is the ADT pattern — the compiler checks exhaustiveness and warns about missing cases.
Sealed Traits and ADTs
// Algebraic Data Types: sealed trait Expr case class Num(value: Double) extends Expr case class Add(l: Expr, r: Expr) extends Expr case class Mul(l: Expr, r: Expr) extends Expr // Exhaustive pattern matching: def eval(e: Expr): Double = e match case Num(v) => v case Add(l, r) => eval(l) + eval(r) case Mul(l, r) => eval(l) * eval(r) // Usage: val expr = Add(Num(2), Mul(Num(3), Num(4))) eval(expr) // 14.0 // Enum (Scala 3 - simple ADT): enum Color: case Red, Green, Blue enum Shape: case Circle(r: Double) case Rectangle(w: Double, h: Double)
sealed trait restricts subclasses to the same file — the compiler checks exhaustiveness in match (warns about missing cases). Combined with case class it forms ADTs (Algebraic Data Types) — the basis of functional programming in Scala. enum (Scala 3) is sugar for simple sealed hierarchies. Ideal for modeling domains with finite variants.
Match / Case
// Basic pattern matching: val msg = number match case 1 => "one" case 2 => "two" case n if n > 10 => "large" case _ => "other" // With types: value match case s: String => s.length case n: Int => n * 2 case _ => 0 // With case classes: shape match case Circle(r) => math.Pi * r * r case Rectangle(w, h) => w * h // With lists: list match case Nil => "empty" case h :: Nil => s"one: $h" case h :: t => s"head: $h" // Guard clauses: case n if n % 2 == 0 => "even"
Pattern matching is the most powerful Scala feature — it combines switch/case, destructuring and type checking. With sealed trait, the compiler checks exhaustiveness (warns about missing cases). Guards (if n > 10) add arbitrary conditions. List patterns (h :: t) decompose recursively — the basis of functional somethingrithms.
Given / Using (Scala 3)
// Given (implicit instance):
given Ordering[Int] with
def compare(x: Int, y: Int) = x - y
// Using (implicit parameter):
def maximum[T](a: T, b: T)(using ord: Ordering[T]): T =
if ord.compare(a, b) > 0 then a else b
// Extension methods:
extension (s: String)
def shout: String = s.toUpperCase + "!"
def words: List[String] = s.split(" ").toList
"hello world".shout // "HELLO WORLD!"
"a b c".words // List("a","b","c")
// Type class with given:
trait Show[T]:
def show(value: T): String
given Show[Int] with
def show(value: Int) = s"Int($value)"given/using (Scala 3) replaces the implicit of Scala 2 — given defines available instances, using injects them automatically. Extension methods add methods to existing types without wrapper classes. Together, they implement the Type Class pattern: polymorphic behavior without inheritance. Clearer and safer than implicit.
For Comprehensions
// Basic (side effect): for i <- 1 to 10 do println(i) // With yield (returns a collection): val squares = for i <- 1 to 10 yield i * i // With filter (guard): val evens = for i <- 1 to 20 if i % 2 == 0 yield i // Multiple generators: val coords = for x <- 1 to 5 y <- 1 to 5 yield (x, y) // With Option: val result = for a <- Some(10) b <- Some(20) yield a + b // Some(30) // Equivalent to map/flatMap/filter
For comprehensions are syntactic sugar for map/flatMap/filter — they work with any type implementing those methods (List, Option, Future, Either). With yield, they return a collection; without yield, they run side effects. Multiple generators produce the cartesian product. None/Left short-circuits automatically.
Try (error handling)
import scala.util.{Try, Success, Failure}
// Try wraps exceptions:
val result = Try("42".toInt)
// Success(42)
val error = Try("abc".toInt)
// Failure(NumberFormatException)
// Pattern matching:
result match
case Success(n) => println(s"Number: $n")
case Failure(e) => println(s"Error: $e")
// getOrElse / recover:
result.getOrElse(0)
result.recover { case _ => 0 }
// map / flatMap:
result.map(_ * 2) // Success(84)
// for comprehension:
val total = for
a <- Try("10".toInt)
b <- Try("20".toInt)
yield a + b // Success(30)Try wraps computations that may throw exceptions — Success(value) or Failure(exception). It is the functional alternative to try/catch: it supports map, flatMap, for comprehensions and pattern matching. recover handles specific errors. For domain errors, prefer Either (more explicit); Try is ideal for wrapping Java APIs.
While and Loops
// While:
var x = 10
while x > 0 do
println(x)
x -= 1
// Do-while (Scala 3):
var input = ""
do
input = readLine()
while input != "quit"
// foreach (prefer over while):
(1 to 10).foreach(println)
List("a","b","c").foreach(println)
// Range with step and direction:
(1 until 10 by 2).foreach(println)
(10 to 1 by -1).foreach(println)
// break (rare, use recursion):
import scala.util.control.Breaks._
breakable {
for i <- 1 to 100 do
if i > 10 then break
}In functional programming, while is discouraged (it requires a mutable var). Prefer foreach, map or recursion. Ranges with by allow custom steps and reverse direction. break exists but is rare — it usually means a functional approach (takeWhile, find, recursion) would be clearer and safer.
Either (typed errors)
// Either: Left (error) or Right (success)
type Result[A] = Either[String, A]
def parse(s: String): Result[Int] =
s.toIntOption match
case Some(n) => Right(n)
case None => Left(s"Invalid: $s")
parse("42") // Right(42)
parse("abc") // Left("Invalid: abc")
// For comprehension (short-circuit):
val total = for
a <- parse("10")
b <- parse("20")
yield a + b // Right(30)
// Pattern matching:
parse("x") match
case Right(n) => println(s"OK: $n")
case Left(msg) => println(s"Error: $msg")
// Transform:
parse("42").map(_ * 2) // Right(84)
parse("x").getOrElse(0) // 0Either represents two possible outcomes: Left (error) and Right (success). It is preferred for domain errors because it documents the error type in the signature. For comprehensions with Either short-circuit on the first Left. Convention: Right = success (right = "correct"). It replaces exceptions for expected errors.
Strings e I/O
String Operations
val s = "Hello World"
// Transformations:
s.toUpperCase // "HELLO WORLD"
s.toLowerCase // "hello world"
s.reverse // "dlroW olleH"
s.trim // removes spaces
// Search:
s.contains("World") // true
s.startsWith("Hello") // true
s.indexOf("World") // 6
s.matches("He.*") // true (regex)
// Substring and split:
s.substring(6) // "World"
s.substring(0, 5) // "Hello"
"a,b,c".split(",") // Array("a","b","c")
// Replacement:
s.replace("World", "Scala")
"aaa".replaceAll("a", "o") // "ooo"
// Repetition and padding:
"ab" * 3 // "ababab"
"42".padTo(5, '0') // "42000"Strings in Scala inherit all methods of java.lang.String plus extensions. split returns Array (use .toList for List). replaceAll accepts regex, replace is literal. matches checks regex on the whole string. Strings are immutable — every operation returns a new string. padTo pads up to a minimum length.
StringBuilder and Performance
// StringBuilder (mutable, efficient):
val sb = new StringBuilder
for i <- 1 to 1000 do
sb.append(s"line $i\n")
val result = sb.toString
// vs concatenation (slow - O(n²)):
var s = ""
for i <- 1 to 1000 do
s += s"line $i\n" // NEW object each iter
// mkString (prefer for collections):
val csv = List("a", "b", "c").mkString(",")
val html = items.map(li).mkString("<ul>\n", "\n", "\n</ul>")
// String.join (Java):
String.join(", ", "a", "b", "c")
// Performance comparison:
// mkString > StringBuilder >> concatenation
// For < 10 elements, concatenation is OKRepeated concatenation with + creates a new object on each iteration — O(n²). StringBuilder is mutable and efficient for incrementsl building. mkString is the idiomatic way for collections (accepts prefix, separator, suffix). For fewer than 10 elements, the difference is negligible. In hot loops with many strings, StringBuilder is essential.
Advanced Interpolation
// s-interpolator:
val name = "Anna"
val age = 30
s"$name is $age years old"
s"${name.toUpperCase}!" // expression
// f-interpolator (formatting):
val pi = 3.14159
f"$pi%.2f" // "3.14"
f"$age%05d" // "00030"
f"$pi%10.3f" // " 3.142"
// raw (no escapes):
raw"C:\new\folder" // literal
// Custom interpolator:
implicit class JsonHelper(val sc: StringContext) extends AnyVal:
def json(args: Any*): String =
s"""{"value": "${sc.s(args*)}"}"""
json"$name" // {"value": "Anna"}
// stripMargin:
val sql = """
|SELECT *
|FROM users
|WHERE age > 18
""".stripMarginScala allows custom interpolators via implicit class on StringContext — libraries like sql, json, uri use this mechanism. The f-interpolator supports all printf formatting (%d, %s, %.2f). stripMargin removes the | prefix in multiline strings. Triple-quotes preserve formatting.
Parsing and Conversion
// Safe parsing (Option):
"42".toIntOption // Some(42)
"abc".toIntOption // None
"3.14".toDoubleOption // Some(3.14)
// Parsing with Try:
import scala.util.Try
Try("42".toInt) // Success(42)
Try("abc".toInt) // Failure(...)
// Parsing with Either:
def parseInt(s: String): Either[String, Int] =
s.toIntOption.toRight(s"Not a number: $s")
// Parsing pipeline:
val result = for
s <- Some(" 42 ")
trimmed <- Some(s.trim)
n <- trimmed.toIntOption
yield n * 2 // Some(84)
// BigDecimal (precision):
val price = BigDecimal("19.99")
val total = price * 3 // 59.97 (exact!)Parsing in Scala should be safe: toIntOption returns Option (no exception), Try catches exceptions, Either documents errors in the type. Pipelines with for comprehension propagate failures automatically. BigDecimal is mandatory for monetary values — Double has precision errors (0.1 + 0.2 != 0.3).
Regex in Scala
import scala.util.matching.Regex
// Create regex:
val pattern = """(\d+)-(\d+)-(\d+)""".r
// findFirstMatchIn:
pattern.findFirstIn("Date: 25-12-2024")
// Some("25-12-2024")
// findAllIn:
val nums = """\d+""".r
nums.findAllIn("a1 b22 c333").toList
// List("1", "22", "333")
// Pattern matching with regex:
"25-12-2024" match
case pattern(d, m, y) => s"$y/$m/$d"
// replaceAllIn:
val clean = """\s+""".r.replaceAllIn("a b c", " ")
// Regex with named groups:
val email = """(\w+)@(\w+)\.(\w+)""".r
email.findFirstMatchIn("ana@mail.com") match
case Some(m) => println(m.group(1)) // "ana"Regex in Scala uses scala.util.matching.Regex — created with .r on any string. Pattern matching with regex extracts groups directly (case pattern(d, m, y)). findAllIn returns an iterator of matches. replaceAllIn replaces all occurrences. Triple-quotes avoid escaping backslashes. It integrates naturally with pattern matching.
Console and Arguments
// Output:
println("With newline")
print("Without newline")
Console.err.println("Error!")
// Formatted output:
printf("%s is %d years old%n", "Anna", 30)
println(f"$pi%.2f")
// Input:
val name = scala.io.StdIn.readLine("Name: ")
val age = scala.io.StdIn.readInt("Age: ")
// CLI arguments:
@main def app(args: String*): Unit =
args.foreach(println)
println(s"Total: ${args.size} args")
// sbt run arg1 arg2
// scala-cli run app.scala -- arg1 arg2
// System properties and env:
sys.props("java.version")
sys.env.getOrElse("HOME", "/tmp")println/print for standard output. Console.err for stderr. scala.io.StdIn for user input (readLine, readInt). @main (Scala 3) with varargs receives CLI arguments. sys.props accesses system properties, sys.env environment variables. printf uses Java formatting.
File I/O
import scala.io.Source
import java.io.PrintWriter
// Read file:
val source = Source.fromFile("data.txt")
val content = source.mkString
source.close()
// Read line by line:
val lines = Source.fromFile("data.txt")
.getLines().toList
// Write:
val writer = new PrintWriter("output.txt")
writer.write("Line 1\n")
writer.println("Line 2")
writer.close()
// Using (auto-close, Scala 2.13+):
val text = Using(Source.fromFile("data.txt")) { src =>
src.mkString
}.getOrElse("")
// java.nio (modern):
import java.nio.file.{Files, Path}
val bytes = Files.readAllBytes(Path.of("data.txt"))
val str = new String(bytes)
Files.writeString(Path.of("out.txt"), "Hello")scala.io.Source is the classic way to read files — always close with close(). Using (2.13+) closes automatically (try-with-resources). java.nio.file.Files is the modern API (more performant for large files). PrintWriter for writing. For complex paths, use the-lib (Li Haoyi library).
Char and Unicode
// Char:
val c = 'A'
c.toInt // 65
c.toLower // 'a'
c.isLetter // true
c.isDigit // false
c.isUpper // true
// Iterate string (chars):
"Café".foreach(c => println(c.toInt))
// Unicode:
'\u0041' // 'A'
"Café".length // 4 (UTF-16 units)
// codePoints (real Unicode):
"Café".codePointCount(0, "Café".length) // 4
// Conversions:
65.toChar // 'A'
('a' to 'z').toList // alphabet
('0' to '9').toList // digits
// Character methods:
Character.isWhitespace(' ') // true
Character.toUpperCase('a') // 'A'Char in Scala is UTF-16 (2 bytes). Methods like isLetter, isDigit, toUpper/toLower are convenient. Char ranges ('a' to 'z') generate sequences. For full Unicode (emoji, CJK), use codePointCount and codePoints. String.length counts UTF-16 units, not codepoints.
Patterns e Idioms
Functional Pipeline
// Transformation pipeline:
val result = data
.map(clean)
.filter(validate)
.map(transform)
.foldLeft(summary)(combine)
// Pipe operator (Scala 3):
val output = input
|> trim
|> toLowerCase
|> (_.replace(" ", "-"))
|> slugify
// Function composition:
val process = trim andThen toLowerCase andThen slugify
val result2 = process(" Hello World ")
// Chaining with tap (side effect):
extension [A](a: A)
def tap(f: A => Unit): A = { f(a); a }
val user = createUser("Anna")
.tap(u => log(s"Created: $u"))
.tap(u => metrics.increment("users"))A pipeline chains transformations without intermediate state — each step receives the result of the previous one. The pipe operator |> (Scala 3) makes the direction explicit. andThen composes functions left-to-right. The tap pattern allows side effects in the pipeline without breaking the flow. More readable and testable than temporary variables.
Type Class Pattern
// 1. Define the type class:
trait Encoder[T]:
def encode(value: T): String
// 2. Instances for specific types:
given Encoder[Int] with
def encode(v: Int) = v.toString
given Encoder[String] with
def encode(v: String) = s""""$v""""
// 3. Derived instance:
given [T](using e: Encoder[T]): Encoder[List[T]] with
def encode(v: List[T]) =
v.map(e.encode).mkString("[", ",", "]")
// 4. Syntax extension:
extension [T](value: T)(using e: Encoder[T])
def toJson: String = e.encode(value)
// 5. Usage:
42.toJson // "42"
"hello".toJson // ""hello""
List(1,2,3).toJson // "[1,2,3]"The Type Class pattern has 5 steps: define the trait, create given instances, derive composed instances, add a syntax extension, use it. It enables ad-hoc polymorphism without inheritance — existing types gain new behavior without modification. It is the central pattern in Cats, Circe, http4s. More flexible than inheritance: one instance per type, no diamond problem.
Immutable Builder
// Builder with case class + copy:
case class Request(
url: String = "",
method: String = "GET",
headers: Map[String, String] = Map.empty,
body: Option[String] = None
):
def withUrl(u: String) = copy(url = u)
def withMethod(m: String) = copy(method = m)
def withHeader(k: String, v: String) =
copy(headers = headers + (k -> v))
def withBody(b: String) = copy(body = Some(b))
// Usage (fluent API):
val req = Request()
.withUrl("https://api.com")
.withMethod("POST")
.withHeader("Auth", "token")
.withBody("""{"key": "value"}""")
// Each with* returns a NEW instance
// Thread-safe, no mutationThe immutable Builder uses case class + copy — each with* returns a new instance (thread-safe, no mutation). Default args make all fields optional. Safer than a mutable Builder (GoF) — inconsistent state is impossible. The pattern is ubiquitous in Scala libraries (Akka, http4s, doobie). Fluent API without side effects.
Error Accumulation
import cats.data.ValidatedNel
import cats.syntax.all._
type Errors[A] = ValidatedNel[String, A]
// Independent validations:
def validateName(s: String): Errors[String] =
if s.nonEmpty then s.validNel
else "Empty name".invalidNel
def validupToge(n: Int): Errors[Int] =
if n >= 0 && n <= 150 then n.validNel
else s"Invalid age: $n".invalidNel
def validateEmail(s: String): Errors[String] =
if s.contains("@") then s.validNel
else "Email without @".invalidNel
// Accumulate ALL errors:
case class User(name: String, age: Int, email: String)
def validateForm(name: String, age: Int, email: String)
: Errors[User] =
(validateName(name), validupToge(age), validateEmail(email))
.mapN(User.apply)
// Either short-circuits (only 1st error)
// Validated accumulates allValidated (Cats) accumulates ALL validation errors — unlike Either which stops at the first. ValidatedNel = Validated[NonEmptyList[E], A]. mapN combines N validations and applies the function if all pass. Ideal for forms (show all errors at once). invalidNel/validNel are syntax sugar.
Strategy with Functions
// Strategy = passing functions:
type Strategy = List[Int] => List[Int]
val ascending: Strategy = _.sorted
val descending: Strategy = _.sorted.reverse
val evensFirst: Strategy = xs =>
xs.filter(_ % 2 == 0) ++ xs.filter(_ % 2 == 1)
def sortData(data: List[Int], s: Strategy) = s(data)
sortData(List(3,1,4,1,5), ascending)
sortData(List(3,1,4,1,5), descending)
// Comparator the a function:
case class User(name: String, age: Int)
val users = List(User("Anna", 30), User("Ray", 25))
users.sortBy(_.age)
users.sortBy(_.name)
users.sortWith((a, b) => a.age > b.age)
// Higher-order strategy:
def retry[T](n: Int)(f: () => T): T =
Try(f()).getOrElse(retry(n - 1)(f))The Strategy pattern in FP is simply passing functions the parameters — a type alias for clarity. It eliminates class/trait hierarchies for variable behavior. sortBy/sortWith accept sorting strategies the functions. Higher-order strategies (retry) encapsulate execution patterns. More concise and composable than OOP.
State Machine Pattern
// States the ADT:
sealed trait DoorState
case object Closed extends DoorState
case object Open extends DoorState
case object Locked extends DoorState
// Events:
sealed trait DoorEvent
case object Push extends DoorEvent
case object Pull extends DoorEvent
case object Lock extends DoorEvent
case object Unlock extends DoorEvent
// Transition (pure function):
def transition(state: DoorState, event: DoorEvent): DoorState =
(state, event) match
case (Closed, Push) => Open
case (Open, Pull) => Closed
case (Closed, Lock) => Locked
case (Locked, Unlock) => Closed
case _ => state // invalid transition
// Run a sequence:
val events = List(Push, Pull, Lock, Unlock)
val final_state = events.foldLeft(Closed: DoorState)(transition)State machines in FP are pure functions (State, Event) => State — no mutation, easily testable. Pattern matching with the tuple (state, event) defines all transitions. foldLeft runs event sequences. Invalid transitions return the current state (or Either for logging). Used in Akka FSM, http4s middleware and UI state.
ADTs and Modeling
// Model the domain with ADTs:
sealed trait Payment
case class Card(number: String, exp: String) extends Payment
case class BankAccount(iban: String) extends Payment
case object Cash extends Payment
// Exhaustive processing:
def process(p: Payment): String = p match
case Card(num, _) => s"Card: ${num.takeRight(4)}"
case BankAccount(iban) => s"IBAN: $iban"
case Cash => "Cash"
// State the ADT:
sealed trait State
case object Loading extends State
case class Loaded(data: List[String]) extends State
case class Error(msg: String) extends State
// Transitions:
def transition(s: State, event: Event): State =
(s, event) match
case (Loading, DataReady(d)) => Loaded(d)
case (Loading, Failed(e)) => Error(e)
case _ => sADTs (Algebraic Data Types) model domains with finite variants — sealed trait + case class/case object. The compiler checks exhaustiveness (impossible to forget a case). Pattern matching with tuples (state, event) models state machines. It replaces complex OOP hierarchies with simpler, safer code. A central pattern in Scala.
Idioms and Conventions
// 1. Prefer val over var // 2. Prefer immutable over mutable // 3. Option instead of null // 4. Either instead of exceptions (domain) // 5. Pattern matching instead of if/else chains // Named args for clarity: connect(host = "localhost", port = 5432) // Default args instead of overloads: def log(msg: String, level: String = "INFO") = ??? // Case class instead of Tuple with meaning: case class Point(x: Double, y: Double) // Extension instead of utility classes: extension (s: String) def slug: String = ??? // for comprehension instead of flatMap chains: val r = for a <- fetchA() b <- fetchB(a) yield combine(a, b) // sealed + match instead of visitor pattern
Scala idioms: val by default, immutable by default, Option instead of null, Either instead of exceptions for expected errors. Named/default args eliminate overloads. Case classes document better than tuples. Extension methods replace utility classes. For comprehensions are more readable than flatMap chains. sealed + match replaces the visitor pattern.