Noções Básicas
Variáveis e Tipos
Prefira val (imutável) em vez de var (mutável) para código mais seguro e previsível. Scala infere tipos, mas anotações explícitas auxiliam a legibilidade para APIs públicas. Tudo é um objeto—sem primitivos (Int, Boolean são classes).
val name = "Alice" // immutable (preferred)
var age = 30 // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List[Int] = List(1, 2, 3)
println(name.getClass) // class java.lang.StringInterpolação de Strings
s"..." habilita interpolação ${expr}. f"..." adiciona formatação estilo printf (%s, %.2f). raw"..." desabilita sequências de escape. Esses prefixos tornam a construção de strings type-safe e legível.
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg") // formatted
println(raw"No \n escape") // raw stringTuplas
Tuplas agrupam 2-22 valores heterogêneos. Acesso via _1, _2 (1-indexado). Destruture com val (a, b) = tuple. Para mais de 2 elementos, prefira case classes para campos nomeados e melhor legibilidade.
val pair = ("Alice", 30)
println(pair._1) // Alice
println(pair._2) // 30
val (name, age) = pair // destructure
println(s"${name}: ${age}")
// Scala 3: val p = ("a", 1, 2.0)
val triple = ("a", 1, 2.0)
println(triple._3) // 2.0Inferência de Tipos e Ascription
Scala infere tipos para variáveis locais e tipos de retorno. Use tipos explícitos para APIs públicas, funções recursivas e casos ambíguos. Type ascription (expr: Type) força um tipo—útil para upcasting ou desambiguação.
val x = 42 // Int inferred
val y: Long = 42 // explicit Long
val z = 42: Long // type ascription
val list = List(1, 2, 3) // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2 // return type inferredUnit e Nothing
Unit é como void (um valor: ()). Nothing é o bottom type sem instâncias—usado para funções que nunca retornam (throw, loop infinito). Nothing é um subtipo de todos os tipos, permitindo inferência de tipos flexível.
def printIt(x: Int): Unit = println(x) // like void
val u: Unit = () // Unit has one value: ()
// Nothing is the bottom type—no instances
def error(msg: String): Nothing =
throw new RuntimeException(msg)
// Nothing is a subtype of everything
val n: Nothing = error("boom")Strings
Métodos Comuns de String
Strings Scala são strings Java com métodos extras via conversões implícitas (StringOps). A maioria dos métodos retorna novas strings (imutáveis). Use-os em vez de loops manuais para clareza e correção.
val s = "Hello, World"
println(s.length) // 12
println(s.toUpperCase) // HELLO, WORLD
println(s.toLowerCase) // hello, world
println(s.split(", ")) // Array(Hello, World)
println(s.replace("o", "0")) // Hell0, W0rld
println(s.reverse) // dlroW ,olleH
println(s.contains("World")) // trueStrings Multilinha (Aspas Triplas)
Strings com aspas triplas preservam todo whitespace e novas linhas. Use stripMargin com | para alinhar código de forma limpa—apenas texto após | é mantido. Ideal para SQL, JSON ou templates embutidos em código.
val sql = """
SELECT * FROM users
WHERE age > 18
ORDER BY name
"""
println(sql.trim)
// StripMargin for clean indentation
val text = """|Hello
|World""".stripMargin
println(text) // Hello
WorldConstrução de Strings
mkString junta coleções com prefix/suffix opcional—idiomático e eficiente. Use StringBuilder para construir strings grandes em loops. Evite concatenação + repetida em loops (cria muitos objetos intermediários).
val parts = List("apple", "banana", "cherry")
println(parts.mkString(", ")) // apple, banana, cherry
println(parts.mkString("[", ", ", "]")) // [apple, banana, cherry]
val sb = new StringBuilder
for (p <- parts) sb.append(p).append(" ")
println(sb.toString.trim)String para Número
toInt/toDouble lançam em entrada inválida. Use toIntOption (Scala 2.13+) para parsing seguro retornando Option. Para parsing em massa, use Try ou Either para tratar erros funcionalmente sem exceções.
val n = "42".toInt // 42
val d = "3.14".toDouble // 3.14
val b = "true".toBoolean // true
val safe = "abc".toIntOption // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result) // 0Regex
.r converte uma string em Regex. findFirstIn retorna Option, findAllIn retorna um iterador. Use em pattern matching com case email(e) => para extração. Regex é o Pattern do Java por baixo dos panos.
import scala.util.matching.Regex
val email: Regex = "[\w.]+@[\w]+\.[a-z]+".r
val text = "Contact: [email protected]"
email.findFirstIn(text) match {
case Some(e) => println(s"Found: ${e}")
case None => println("No email")
}
val replaced = "[0-9]+".r.replaceAllIn("a1b2c3", "#")
println(replaced) // a#b#c#Estruturas de Dados
List e Seq
List é uma lista imutável simplesmente encadeada—O(1) head/prepend, O(n) acesso aleatório. Use Vector para acesso aleatório (O(1) efetivo). +: prepende, :+ anexa. Prefira coleções imutáveis para thread safety.
val nums = List(1, 2, 3, 4, 5)
println(nums.head) // 1
println(nums.tail) // List(2,3,4,5)
println(nums.reverse) // List(5,4,3,2,1)
println(nums.take(2)) // List(1, 2)
println(nums.drop(2)) // List(3, 4, 5)
println(nums.mkString) // 12345
val combined = 0 +: nums :+ 6 // List(0,1,2,3,4,5,6)Map
Map é imutável—operações retornam novos Maps. Use get(key) para acesso Option, getOrElse para padrões. + adiciona/atualiza, - remove. Para maps mutáveis, use scala.collection.mutable.Map. Chaves devem ser Hashable.
val ages = Map("Alice" -> 30, "Bob" -> 25)
println(ages("Alice")) // 30 (throws if missing)
println(ages.getOrElse("Eve", 0)) // 0 (safe)
val updated = ages + ("Eve" -> 28) // new Map
val removed = ages - "Bob"
ages.foreach { case (k, v) => println(s"${k}: ${v}") }
println(ages.keys) // Set(Alice, Bob)Set
Set é imutável com O(1) contains. union (|), intersect (&), diff (~) para operações de conjunto. + adiciona, - remove. Use para desduplicação e teste de pertinência. Variante mutável: scala.collection.mutable.Set.
val a = Set(1, 2, 3)
val b = Set(3, 4, 5)
println(a union b) // Set(1,2,3,4,5)
println(a intersect b) // Set(3)
println(a diff b) // Set(1,2)
println(a subsetOf(Set(1,2,3,4))) // true
val added = a + 6 // Set(1,2,3,6)Option (Null Safety)
Option substitui null—Some(value) ou None. Use map/filter/flatMap para transformações, getOrElse para padrões. For-comprehensions funcionam em Option. Isso elimina NullPointerException ao tornar a ausência explícita no tipo.
def findUser(id: Int): Option[String] =
if (id == 1) Some("Alice") else None
val name = findUser(1)
println(name.getOrElse("Unknown")) // Alice
println(name.map(_.toUpperCase)) // Some(ALICE)
println(name.filter(_.startsWith("A"))) // Some(Alice)
val result = for {
n <- findUser(1)
if n.startsWith("A")
} yield n.toUpperCase // Some(ALICE)Array e Vector
Array é um array Java mutável (mais rápido, mas sem atualizações funcionais). Vector é imutável com acesso aleatório e atualizações efetivamente O(1)—preferido para coleções imutáveis de acesso aleatório. Use List para sequencial, Vector para indexado.
val arr = Array(1, 2, 3, 4) // mutable, Java array
arr(0) = 10
println(arr(0)) // 10
val vec = Vector(1, 2, 3, 4) // immutable, fast random access
println(vec(2)) // 3
val updated = vec.updated(0, 10) // Vector(10,2,3,4)
// Vector: O(1) random access + immutable
// Array: mutable, Java interop, fastestFluxo de Controle
If / Else (Expressão)
Em Scala, if/else é uma expressão que retorna um valor. Isso elimina a necessidade de um operador ternário. Ambas as ramificações devem ter tipos compatíveis. Use isso para atribuição condicional concisa.
val score = 85
val grade =
if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else "F"
println(grade) // B
// if returns a value—no ternary operator neededFor Comprehension
for comprehensions iteram e podem filtrar (if guards) e transformar (yield). Sem yield, é um loop; com yield, constrói uma coleção. to inclui o final, until exclui. Equivalente a cadeias flatMap/map/filter.
for (i <- 1 to 5) println(i) // 1 2 3 4 5
for (i <- 1 until 5) print(i) // 1 2 3 4
// With yield (generates collection)
val doubled = for (n <- List(1,2,3)) yield n * 2
println(doubled) // List(2, 4, 6)
// With filter (guard)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(evens.toList) // List(2,4,6,8,10)Match (Pattern Matching)
match é o poderoso pattern matching de Scala—como switch nos esteroides. Suporta literais, OR (|), guards (if), correspondência de tipo e destructuring. Deve ser exaustivo (compilador avisa sobre cases faltantes). É uma expressão que retorna um valor.
val n = 2
val label = n match {
case 0 => "zero"
case 1 | 2 | 3 => "small"
case x if x < 10 => "medium"
case _ => "large"
}
println(label) // small
// Match on types
def describe(x: Any): String = x match {
case i: Int => s"Int: ${i}"
case s: String => s"String: ${s}"
case _ => "unknown"
}While e Do-While
while/do-while são loops imperativos que retornam Unit. Requerem estado mutável (var). Prefira for-comprehensions ou recursão para Scala funcional idiomático. Use while apenas quando o desempenho exigir ou para efeitos colaterais.
var i = 0
while (i < 3) {
println(i)
i += 1
}
var j = 0
do {
println(j)
j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutabilityTry / Catch / Finally
try/catch/finally é como Java, mas usa pattern matching no catch. Prefira Try para tratamento de erros funcional—envolve exceções como valores Success/Failure, habilitando cadeias map/flatMap sem boilerplate try/catch.
import scala.util.{Try, Success, Failure}
val result = try {
"abc".toInt
} catch {
case e: NumberFormatException => 0
} finally {
println("cleanup")
}
println(result) // 0
// Functional alternative
val r2 = Try("abc".toInt).getOrElse(0)
println(r2) // 0Funções
Definição de Método
Métodos usam def name(params): ReturnType = body. Parâmetros padrão e argumentos nomeados são suportados. Retorno Unit = apenas efeito colateral. Corpos de expressão única omitem chaves. = é obrigatório (sem ele, é um procedimento retornando Unit).
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4)) // 7
println(greet("Alice")) // Hello, Alice!
println(greet("Bob", greeting = "Hi")) // named argLambda (Função Anônima)
Lambdas: (params) => body. Use _ como abreviação para parâmetros únicos (x => x * 2 torna-se _ * 2). Múltiplos _ referem-se a parâmetros diferentes (_ + _). Lambdas são first-class—passe-os para map, filter, reduce, etc.
val square = (x: Int) => x * x
println(square(5)) // 25
val nums = List(1, 2, 3)
println(nums.map(_ * 2)) // List(2, 4, 6)
println(nums.filter(_ > 1)) // List(2, 3)
println(nums.reduce(_ + _)) // 6
// _ is shorthand for the parameterFunções de Ordem Superior
Funções de ordem superior recebem ou retornam funções. Isso permite abstrações poderosas: map/filter/reduce, composição, aplicação parcial. makeAdder retorna uma closure capturando n. Este é o coração da programação funcional.
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5)) // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10)) // 15
// Functions returning functions = currying-likeCurrying e Aplicação Parcial
Currying divide parâmetros em múltiplas listas: def f(a)(b). Aplique parcialmente com _ para criar funções especializadas. Múltiplas listas de parâmetros melhoram a inferência de tipos (o compilador pode inferir tipos de f a partir da lista). Comum em APIs de coleção.
def add(a: Int)(b: Int): Int = a + b // curried
val add5 = add(5)_ // partially applied
println(add5(3)) // 8
// Multiple parameter lists
def foldLeft[A, B](list: List[A])(z: B)(f: (B, A) => B): B = ???
// Helps type inference
val sum = List(1,2,3).foldLeft(0)(_ + _)By-Name e Lazy
Parâmetros by-name (=> T) são avaliados lazymente a cada uso—úteis para logging (pula msg cara se desabilitado) e estruturas de controle customizadas. lazy val adia a inicialização até o primeiro acesso—use para valores caros ou opcionais.
// By-name parameter: evaluated on each use
def debug(msg: => String): Unit =
if (debugEnabled) println(msg)
// Lazy evaluation
lazy val expensive = computeHeavy()
println(expensive) // computed now
def computeHeavy(): Int = { println("computing"); 42 }Classes e POO
Classe e Construtor
Parâmetros do construtor primário estão na assinatura da classe. Parâmetros val tornam-se campos imutáveis (getter público), var mutáveis. O corpo da classe É o construtor. Construtores auxiliares usam def this(...) e devem chamar outro construtor.
class Person(val name: String, val age: Int) {
// Constructor params with val/var become fields
def greet: String = s"Hi, I'm ${name}"
def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet) // Hi, I'm Alice
println(p.name) // Alice (val field)
println(p.isAdult) // trueCase Class
Case classes são classes de dados imutáveis com equals, hashCode, toString, copy e companion object com apply/unapply gerados automaticamente. Use para modelagem de dados e pattern matching. Sem 'new' necessário. Elas são a fundação de ADTs em Scala.
case class Point(x: Int, y: Int)
val p1 = Point(3, 4) // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2) // true (value equality)
val moved = p1.copy(x = 5) // Point(5, 4)
println(p1.x, p1.y) // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companionTraits (Interfaces com Implementação)
Traits são como interfaces Java, mas podem ter implementação. Uma classe pode misturar múltiplos traits (com extends/with). Traits permitem herança múltipla de comportamento. Use para interfaces compartilhadas, mixins e modificações empilháveis via linearização.
trait Greetable {
def name: String // abstract
def greet: String = s"Hello, ${name}" // concrete
}
trait Named {
val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet) // Hello, Alice
// Stackable traits via linearizationObject (Singleton)
object declara um singleton (uma instância). Companion objects (mesmo nome de uma classe) contêm métodos semelhantes a static, factories (apply) e extractors (unapply). Use apply para factory methods para que chamadores omitam 'new'. Isso é Scala idiomático.
object Config {
val version = "1.0"
def load(): Map[String, String] = Map("key" -> "value")
}
println(Config.version) // 1.0
// Companion object (same name as class)
class Person(val name: String)
object Person {
def apply(name: String): Person = new Person(name)
}
val p = Person("Alice") // uses apply, no 'new'Herança e Classe Abstrata
abstract class pode ter membros não implementados. Use extends para herdar, override para redefinir. Prefira traits para mixins (herança múltipla). Use abstract class quando precisar de parâmetros de construtor ou quiser um tipo base. Herança única de classes.
abstract class Shape {
def area: Double // abstract
def describe: String = s"Area: ${area}"
}
class Circle(r: Double) extends Shape {
def area: Double = math.Pi * r * r
}
val c = new Circle(5)
println(c.describe) // Area: 78.53...
// override required for concrete members
// abstract class vs trait: use abstract for base, trait for mixinsColeções e Funcional
Map / Filter / Fold
map transforma, filter seleciona, reduce/foldLeft agrega. Esses são o núcleo da transformação funcional de dados. foldLeft recebe uma seed e é associativo; reduce requer não-vazio. Use-os em vez de loops para clareza e imutabilidade.
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2)) // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0)) // List(2,4)
println(nums.reduce(_ + _)) // 15
println(nums.foldLeft(0)(_ + _)) // 15
println(nums.sum) // 15
println(nums.mkString(", ")) // 1, 2, 3, 4, 5FlatMap e For-Comprehension
flatMap mapeia e aplaina em um passo—essencial para coleções aninhadas e operações monádicas. For-comprehensions são syntactic sugar para cadeias flatMap/map/filter. Use para transformações aninhadas complexas—é mais legível.
val nested = List(List(1, 2), List(3, 4))
println(nested.flatten) // List(1,2,3,4)
println(nested.flatMap(_.map(_ * 2))) // List(2,4,6,8)
// Equivalent for-comprehension:
val result = for {
inner <- nested
n <- inner
} yield n * 2
println(result) // List(2,4,6,8)Agrupamento e Ordenação
groupBy particiona por uma chave em um Map. sorted ordena naturalmente, sortBy por uma função chave, sortWith com um comparator. Esses retornam novas coleções (imutáveis). Use para análise de dados, categorização e ordenação.
val words = List("apple", "bat", "cat", "ant")
val byFirst = words.groupBy(_.head)
// Map(a -> List(apple, ant), b -> List(bat), c -> List(cat))
println(byFirst)
val sorted = words.sorted // List(ant, apple, bat, cat)
val byLen = words.sortBy(_.length) // List(ant, bat, cat, apple)
val desc = words.sortWith(_ > _) // descending
println(sorted, byLen)Either e Try
Try envolve exceções como valores Success/Failure. Either representa Left (erro) ou Right (sucesso)—use para erros de domínio onde você quer distinguir tipos de erro. Ambos suportam map/flatMap para propagação funcional de erros.
import scala.util.{Try, Success, Failure}
def parse(s: String): Try[Int] = Try(s.toInt)
parse("42") match {
case Success(n) => println(s"OK: ${n}")
case Failure(e) => println(s"Err: ${e.getMessage}")
}
// Either for domain errors
def divide(a: Int, b: Int): Either[String, Int] =
if (b == 0) Left("div by zero") else Right(a / b)
println(divide(10, 2)) // Right(5)
println(divide(10, 0)) // Left(div by zero)Coleções Lazy (View e Lazy)
.view torna coleções lazy—operações são adiadas até forçadas (toList, sum, etc.). Evita coleções intermediárias para melhor desempenho em dados grandes. LazyList (antigo Stream) permite sequências infinitas—elementos computados sob demanda.
val nums = (1 to 1000000).view
val result = nums
.filter(_ % 2 == 0)
.map(_ * 2)
.take(5)
.toList // forces evaluation
println(result) // List(4, 8, 12, 16, 20)
// view = lazy, no intermediate collections
// LazyList (Stream) for infinite sequences
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)Pattern Matching
Correspondência de Case Classes
Sealed traits + case classes formam Algebraic Data Types (ADTs). O compilador verifica exaustividade—adicione um case e ele avisa onde matches precisam de atualização. Pattern matching destrutura case classes diretamente. Isso é modelagem Scala idiomática.
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Square(s) => s * s
case Rect(w, h) => w * h
}
println(area(Circle(5))) // 78.53...Guards e Condições
Guards (if condition) adicionam verificações em tempo de execução a padrões. Tornam o matching mais expressivo. A ordem importa—primeira correspondência vence. Use guards para ranges, condições ou lógica complexa que padrões simples não conseguem expressar.
val n = 15
val desc = n match {
case x if x < 0 => "negative"
case 0 => "zero"
case x if x % 2 == 0 => "even"
case _ => "odd"
}
println(desc) // odd
// Guards add boolean conditions to patternsCorrespondência de Coleções
:: padrão cons destrutura listas em head e tail. _* corresponde a zero ou mais elementos em arrays/listas. Esses padrões permitem processamento recursivo de listas e decomposição estrutural. Poderoso para parsing e traversal de árvores.
val list = List(1, 2, 3, 4)
list match {
case Nil => println("empty")
case head :: Nil => println(s"one: ${head}")
case head :: tail => println(s"head=${head}, rest=${tail}")
case _ => println("other")
}
// head :: tail destructures a list
val arr = Array(1, 2, 3)
arr match {
case Array(1, _*) => println("starts with 1")
case _ => println("other")
}Correspondência de Option e Either
Pattern matching em Option/Either é idiomático—Some(x)/None, Right(x)/Left(e). For-comprehensions desugam para flatMap com match. Isso faz o tratamento de erros fluir naturalmente sem verificações if-else explícitas.
def find(id: Int): Option[String] =
if (id == 1) Some("Alice") else None
find(1) match {
case Some(name) => println(s"Found: ${name}")
case None => println("Not found")
}
// In for-comprehensions
val result = for {
name <- find(1)
upper = name.toUpperCase
} yield upper
println(result) // Some(ALICE)Extractors (unapply)
Extractors customizados via unapply permitem pattern matching em qualquer tipo. O método unapply retorna Option dos valores extraídos. Isso permite definir seus próprios padrões—poderoso para DSLs e parsing. Case classes geram unapply automaticamente.
object Email {
def unapply(s: String): Option[(String, String)] = {
val parts = s.split("@")
if (parts.length == 2) Some((parts(0), parts(1))) else None
}
}
"[email protected]" match {
case Email(user, domain) =>
println(s"User: ${user}, Domain: ${domain}")
case _ => println("Not an email")
}Generics e Implicits
Classes e Métodos Genéricos
Generics (parâmetros de tipo [A]) escrevem código type-safe e reutilizável. Use [A] para um único tipo, [A, B] para dois. A inferência de tipos geralmente descobre o tipo. Generics são apagados em runtime (limitação da JVM), mas verificados em tempo de compilação.
class Stack[A] {
private var items: List[A] = Nil
def push(x: A): Unit = { items = x :: items }
def pop: Option[A] = items.headOption
}
val s = new Stack[Int]
s.push(1); s.push(2)
println(s.pop) // Some(2)
def first[A](list: List[A]): Option[A] = list.headOption
println(first(List("a", "b"))) // Some(a)Type Bounds
<: upper bound (A é um subtipo), >: lower bound (A é um supertipo). Context bounds (A: Ordering) exigem um valor implícito daquele tipo. View bounds (A <% B) estão depreciados—use context bounds em vez. Esses restringem parâmetros de tipo.
// Upper bound: A must be Animal or subclass
class Box[A <: Animal](val content: A)
// Lower bound: A must be Dog or superclass
class Kennel[A >: Dog](val occupant: A)
// Context bound: A must have an Ordering
def max[A: Ordering](a: A, b: A): A =
if (implicitly[Ordering[A]].gt(a, b)) a else b
abstract class Animal { def name: String }
class Dog extends Animal { def name = "Rex" }Parâmetros Implícitos
Parâmetros implícitos são injetados pelo compilador a partir do escopo. Use para configuração, type classes ou dependências que você não quer passar por toda parte. Declare com implicit val/def. O compilador pesquisa o escopo envolvente e companion objects.
def greet(name: String)(implicit greeting: String): String =
s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice")) // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi")) // Hi, Bob! (explicit override)
// Compiler finds implicit in scopeType Classes (Conversões Implícitas)
Type classes (via implicits) adicionam comportamento a tipos sem modificá-los—polimorfismo ad-hoc. Defina uma trait, forneça instâncias implícitas e use parâmetros implícitos. É assim que Ordering, Numeric e Show funcionam. Mais flexível que herança.
trait Show[A] { def show(a: A): String }
object Show {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val strShow: Show[String] = identity
}
def printIt[A](a: A)(implicit s: Show[A]): Unit =
println(s.show(a))
printIt(42) // 42
printIt("hello") // hello
// Type class: ad-hoc polymorphismExtension Methods (Scala 2)
Classes implícitas adicionam extension methods a tipos existentes. Defina implicit class com um único parâmetro, e seus métodos tornam-se disponíveis naquele tipo. Use para adicionar métodos utilitários a Int, String, etc. Scala 3 usa a sintaxe 'extension' mais limpa.
implicit class IntOps(val n: Int) extends AnyVal {
def times(f: => Unit): Unit = (1 to n).foreach(_ => f)
def squared: Int = n * n
}
5.times { print("hi") } // hihihihihi
println(5.squared) // 25
// Scala 3: extension (n: Int) def squared = n * nConcorrência e Future
Future e Async
Future representa uma computação assíncrona. onComplete registra um callback. Nunca bloqueie (Await.result) em servidores web de produção—ocupa threads. Use for-comprehensions para encadear futures funcionalmente. Requer um ExecutionContext.
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
Thread.sleep(1000)
42
}
f.onComplete {
case scala.util.Success(v) => println(s"Got ${v}")
case scala.util.Failure(e) => println(s"Err: ${e}")
}
// Don't block in production—use callbacks or for-comprehensionsCompondo Futures
For-comprehensions em Futures executam-nos sequencialmente (cada um espera o anterior). Para execução paralela, inicie todos os Futures primeiro, depois use Future.sequence para combinar. Future.traverse mapeia e sequencia em um passo. Esta é a maneira idiomática de compor trabalho assíncrono.
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f1 = Future { 10 }
val f2 = Future { 20 }
val sum = for {
a <- f1
b <- f2
} yield a + b // Future(30)
// Parallel execution
val results = Future.sequence(List(
Future { 1 }, Future { 2 }, Future { 3 }
))
results.map(_.sum) // Future(6)Coleções Paralelas
.par converte uma coleção em uma versão paralela—operações usam múltiplas threads automaticamente. Bom para trabalho CPU-bound em coleções grandes. Cuidado: operações não associativas (como subtração) podem dar resultados diferentes. Não para trabalho I/O-bound.
val nums = (1 to 1000000).toList
val sum = nums.par.sum // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collectionsPromise (Future Manual)
Promise é o lado gravável de um Future—você o completa manualmente com success/failure. Use ao fazer ponte de APIs baseadas em callback para Futures, ou quando precisar completar um Future de múltiplos lugares. Future é read-only; Promise é write-once.
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println) // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read sideSync vs Async (Await)
Await.result bloqueia a thread atual até o Future concluir (com timeout). Use apenas em testes ou métodos main—bloquear em código assíncrono derrota o propósito. Em produção, use callbacks (onComplete, map) ou for-comprehensions para permanecer non-blocking.
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result) // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks insteadImplicits Profundo
Parâmetros Implícitos
Parâmetros implícitos são passados automaticamente pelo compilador quando um valor implícito do tipo correspondente está no escopo. Isso reduz boilerplate para parâmetros de 'contexto' (ExecutionContext, logging, configuração). O compilador pesquisa: escopo local, companion objects, implicit scope. Você sempre pode passar explicitamente para sobrescrever. Excesso torna o código difícil de rastrear—use para dependências genuinamente contextuais.
import scala.concurrent.ExecutionContext
// Method with implicit parameter
def process[A](data: List[A])
(implicit ec: ExecutionContext): Future[Unit] = {
Future { data.foreach(println) }
}
// The compiler finds an implicit ExecutionContext in scope
implicit val ec: ExecutionContext = ExecutionContext.global
process(List(1, 2, 3)) // ec passed automatically
// Explicitly providing (overrides implicit)
process(List(1, 2, 3))(myCustomEC)
// Multiple implicit parameters
def log(msg: String)(implicit
level: Level, logger: Logger): Unit = {
logger.log(level, msg)
}Conversões Implícitas
Conversões implícitas convertem automaticamente entre tipos quando necessário. implicit class (estendendo AnyVal para zero overhead) adiciona extension methods a tipos existentes—é assim que Scala adiciona métodos a Int, String, etc. Cuidado: conversões implícitas podem tornar o código confuso (o que está sendo convertido?). Prefira implicit classes para extensões em vez de implicit defs brutos. Habilite com import scala.language.implicitConversions.
import scala.language.implicitConversions
// Implicit conversion: one type to another
implicit def intToString(n: Int): String = n.toString
val s: String = 42 // intToString(42) called implicitly
// Extension via implicit class (Scala 2.10+)
implicit class RichInt(val self: Int) extends AnyVal {
def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
def squared: Int = self * self
}
5.times { println("hi") } // prints hi 5 times
3.squared // 9
// Implicit conversion for type compatibility
implicit def javaToScalaList(jl: java.util.List[Int]): List[Int] =
import scala.jdk.CollectionConverters._
jl.asScala.toList
val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList // convertedPrioridade de Resolução Implícita
O compilador resolve implicits por prioridade: escopo local > companion objects > importados > herdados. Se dois implicits do mesmo tipo estão igualmente no escopo, você recebe um erro de 'ambiguous implicit'. O padrão de trait LowPriorityImplicits fornece padrões que podem ser sobrescritos por implicits mais específicos. Entender a ordem de resolução é crucial para design de bibliotecas—coloque padrões em traits de baixa prioridade para que usuários possam sobrescrever.
// Priority of implicit resolution (highest to lowest):
// 1. Local implicit (defined in current scope)
implicit val ec1: ExecutionContext = ec1
// 2. Implicit in companion object
object MyService {
implicit val ec2: ExecutionContext = ec2 // lower priority
}
// 3. Implicit scope (imported)
import somePackage.Implicits._
// 4. Implicit parameter default (inherited trait)
trait DefaultEc {
implicit val ec: ExecutionContext = ExecutionContext.global
}
// More specific type wins
implicit def ord1: Ordering[Int] = ???
implicit def ord2: Ordering[Int] = ??? // ambiguous error!
// LowPriorityImplicits trait pattern
object MyLib {
implicit val high: Ordering[Int] = ???
}
object MyLib extends LowPriorityImplicits
trait LowPriorityImplicits {
implicit val low: Ordering[Int] = ??? // fallback
}Context Bounds e Evidence
Context bounds [A: TypeClass] são syntactic sugar para parâmetros implícitos—eles afirmam que um TypeClass[A] implícito existe. Use implicitly[TypeClass[A]] (Scala 2) ou summon[TypeClass[A]] (Scala 3) para recuperá-lo. Context bounds tornam restrições de type class legíveis: def sort[A: Ordering]. Múltiplos bounds empilham: [A: Ordering: Numeric]. Esta é a maneira idiomática de expressar requisitos de type class.
// Context bound: [A: Ordering] means there's an implicit Ordering[A]
def max[A: Ordering](a: A, b: A): A = {
val ord = implicitly[Ordering[A]] // retrieve the implicit
if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
if (ord.gt(a, b)) a else b
// summon (Scala 3) instead of implicitly
def max3[A: Ordering](a: A, b: A): A = {
val ord = summon[Ordering[A]]
if (ord.gt(a, b)) a else b
}
// Type class evidence
def sort[A: Ordering](list: List[A]): List[A] =
list.sorted // uses the implicit Ordering
// Multiple context bounds
def process[A: Ordering: Numeric](x: A, y: A): A = ???Implicit Scope e Companion Objects
Implicit scope é mais amplo que apenas o escopo atual—inclui companion objects dos tipos envolvidos. É por isso que você não precisa importar Ordering[Int]: ele vive no companion de Int. Esse mecanismo torna as type classes ergonômicas: defina a instância no companion do tipo, e ela é automaticamente disponível. Package objects contêm implicits compartilhados para um package. Esse design permite uso de type class sem imports.
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
implicit val ordering: Ordering[UserId] =
Ordering.by(_.value)
}
// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion
// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters
// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }
// Package object for shared implicits
package object myapp {
implicit val ec: ExecutionContext = ExecutionContext.global
type Id = Long
}Type Classes
Definindo uma Type Class
Uma type class é uma trait parametrizada por tipo, com instâncias fornecendo comportamento para tipos específicos. Diferente da herança, você pode adicionar instâncias de type class retroativamente (para tipos que você não possui). Show[A] define como exibir A. Instâncias vivem no companion object (implicit scope automático). Isso é polimorfismo ad-hoc—comportamento diferente por tipo sem modificar os tipos. Type classes são a abstração mais poderosa de Scala.
// Type class: a trait parameterized by type
trait Show[A] {
def show(a: A): String
}
// Instances for specific types
object Show {
// Instance for Int
implicit val showInt: Show[Int] = (a: Int) => a.toString
// Instance for String
implicit val showString: Show[String] = (s: String) => s""$s""
// Instance for List (recursive)
implicit def showList[A](implicit s: Show[A]): Show[List[A]] =
(list: List[A]) => list.map(s.show).mkString("[", ", ", "]")
}
// Usage with implicit parameter
def print[A](a: A)(implicit s: Show[A]): Unit =
println(s.show(a))
print(42) // 42
print("hello") // "hello"
print(List(1, 2, 3)) // [1, 2, 3]Usando Type Classes (syntactic sugar)
Context bounds [A: Show] + summon recuperam instâncias de type class. Extension methods (implicit class) adicionam métodos como .show que usam a type class. Essa combinação fornece uma API limpa: 42.show funciona se Show[Int] existe. A biblioteca padrão fornece muitas type classes: Numeric, Ordering, Eq, Monoid (Cats). Importar syntax (Numeric.Implicits._) adiciona operadores como + e sum que usam a type class.
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
items.foreach(a => println(summon[Show[A]].show(a)))
// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
def show(implicit s: Show[A]): String = s.show(a)
}
42.show // "42"
"hi".show // ""hi""
List(1,2).show // "[1, 2]"
// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show
// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
summon[Numeric[A]].zero)(summon[Numeric[A]].plus))
// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sumType Classes Comuns (Cats/Scalaz)
Cats e Scalaz fornecem type classes padrão. Monoid (empty + combine) permite agregação genérica. Functor (map) e Monad (pure + flatMap) abstraem sobre containers (List, Option, Future, IO). Eq fornece igualdade type-safe (sem comparações cross-type acidentais). Esses compõem: um Monad é um Functor, um Monoid é um Semigroup. Type classes permitem escrever código genérico e reutilizável que funciona em muitos tipos.
// Monoid: combine values with empty
trait Monoid[A] {
def empty: A
def combine(a: A, b: A): A
}
object Monoid {
implicit val intAdd: Monoid[Int] = new Monoid[Int] {
def empty = 0
def combine(a: Int, b: Int) = a + b
}
implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
def empty = Nil
def combine(a: List[A], b: List[A]) = a ++ b
}
}
// Functor: map over structure
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Monad: chain operations
trait Monad[F[_]] {
def pure[A](a: A): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}
// Eq: type-safe equality
trait Eq[A] {
def eqv(a: A, b: A): Boolean
}
// Semigroup: combine (no empty)
trait Semigroup[A] {
def combine(a: A, b: A): A
}Leis e Testes de Type Classes
Leis de type class são propriedades matemáticas que instâncias devem satisfazer. Monoid exige associatividade e identidade. Functor exige identidade e preservação de composição. Bibliotecas como Cats fornecem definições de leis; discipline + ScalaCheck as testam automaticamente. Leis são o motivo pelo qual type classes são poderosas: código genérico (como foldMap) funciona corretamente para qualquer instância que respeita as leis. Sempre verifique se suas instâncias satisfazem as leis—bugs em instâncias quebram todo código genérico que as usa.
// Type class laws: properties that must hold
// Monoid laws:
// 1. Left identity: combine(empty, a) == a
// 2. Right identity: combine(a, empty) == a
// 3. Associativity: combine(a, combine(b, c)) == combine(combine(a, b), c)
// Functor laws:
// 1. Identity: map(fa)(identity) == fa
// 2. Composition: map(fa)(f andThen g) == map(map(fa)(f))(g)
// Testing laws with ScalaCheck (discipline)
import org.scalacheck.Prop.forAll
import cats.kernel.laws.MonoidLaws
class MonoidSpec extends munit.FunSuite with Discipline {
checkAll("Int Monoid", MonoidLaws[Int].monoid)
}
// Custom law test
def monoidLeftIdentity[A](implicit m: Monoid[A], arb: Arbitrary[A]) =
forAll { (a: A) =>
m.combine(m.empty, a) == a
}
// Laws make type classes trustworthy:
// if an instance satisfies laws, generic code works correctlyDerivação de Type Class (Scala 3)
Scala 3 simplifica a derivação de type class com a palavra-chave 'derives' e Mirror. O compilador pode auto-gerar instâncias para case classes e enums compondo instâncias de elemento. Isso elimina o boilerplate de escrever instâncias para toda case class (comum no Scala 2 com shapeless). Bibliotecas como Cats e Circe suportam derivação do Scala 3. O tipo Mirror dá acesso em tempo de compilação à estrutura de um tipo (tipos de campo, labels) para programação genérica.
// Scala 3: derive type class instances automatically
import scala.deriving.Mirror
trait Show[A] {
def show(a: A): String
}
object Show {
// Inline given for derivation
given showInt: Show[Int] with
def show(a: Int) = a.toString
given showString: Show[String] with
def show(s: String) = s""$s""
// Derive for products (case classes)
given showProduct[A](using m: Mirror.ProductOf[A])
(using ev: Show[m.MirroredElemTypes]): Show[A] with
def show(a: A): String = ???
// Or use Scala 3's derivation
inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}
// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automaticallyFor Comprehensions Profundo
For Comprehensions Básicos
For comprehensions são syntactic sugar para flatMap/map/withFilter. Cada <- é flatMap (exceto o último, que é map). if guards tornam-se withFilter. yield faz retornar uma coleção; omitir yield torna-o imperativo (foreach). Isso funciona em qualquer tipo com flatMap/map (Monad): List, Option, Future, Try, IO. Dominar for-comprehensions é chave para Scala idiomático—eles substituem maps/flatMaps aninhados por sintaxe sequencial legível.
// For comprehension: syntactic sugar for flatMap/map
val result = for {
x <- List(1, 2, 3)
y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)
// Desugared:
List(1, 2, 3).flatMap { x =>
List(10, 20).map { y => x + y }
}
// With filters (if guards)
val evens = for {
x <- 1 to 10
if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)
// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)
// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x) // 1, 2, 3For com Option e Future
For-comprehensions funcionam em qualquer Monad. Com Option, eles short-circuit em None (retornam None). Com Future, eles short-circuit em falha. Isso faz código assíncrono/pode-falhar ler como código imperativo em linha reta, permanecendo funcional. Cada linha <- pode depender de bindings anteriores. Isso é muito mais limpo que chamadas flatMap aninhadas. A mesma sintaxe funciona para Try, Either, IO e monads customizados.
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???
val email: Option[String] = for {
user <- getUser(42)
email <- getEmail(user)
} yield email
// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))
// Future: chain async operations
val result: Future[Int] = for {
user <- fetchUser(1) // Future[User]
posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size
// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic compositionFor com Either e Tratamento de Erros
Either é o tratamento de erros tipado de Scala. For-comprehensions encadeiam Eithers, short-circuitando em Left (erro). Isso é tratamento de erros funcional—sem exceções, erros são valores. O tipo Left é o erro (geralmente String ou uma sealed trait). Either é right-biased no Scala 2.12+ (map/flatMap operam em Right). Esse padrão substitui try/catch por propagação de erros composável e type-safe. Validated do Cats é uma alternativa para acumular erros.
// Either for error handling
def parseAge(s: String): Either[String, Int] =
s.toIntOption.toRight(s"not a number: $s")
def validateAge(age: Int): Either[String, Int] =
if (age >= 0) Right(age) else Left(s"negative: $age")
val result: Either[String, Int] = for {
age <- parseAge("30") // Right(30)
valid <- validateAge(age) // Right(30)
} yield valid
// With error in chain
val error: Either[String, Int] = for {
age <- parseAge("abc") // Left("not a number: abc")
valid <- validateAge(age) // skipped!
} yield valid
// error == Left("not a number: abc")
// Scala 3: for-comprehensions work with Either directly
// (Scala 2 needed either.map(_.right) or withFilter)Desugaring e Monads Customizados
Qualquer tipo com flatMap e map suporta for-comprehensions—este é o padrão Monad. Defina esses métodos no seu tipo para habilitar for-syntax. O = (não <-) cria um binding local (não desugado para flatMap). Entender desugaring ajuda a depurar comprehensions complexos e implementar monads customizados. O compilador traduz for-comprehensions para cadeias flatMap/map/withFilter. É por isso que for funciona uniformemente em List, Option, Future, IO, etc.
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions
case class Box[A](value: A) {
def map[B](f: A => B): Box[B] = Box(f(value))
def flatMap[B](f: A => Box[B]): Box[B] = f(value)
def withFilter(p: A => Boolean): Box[A] =
if (p(value)) this else throw new NoSuchElementException
}
val result = for {
x <- Box(10)
y <- Box(20)
if x < y
} yield x + y
// Box(30)
// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2. Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
// Box(20).withFilter(y => x < y).map(y => x + y))
// Assignment within for (= instead of <-)
for {
x <- List(1, 2, 3)
doubled = x * 2 // local val
} yield doubledFor vs Map/FlatMap (quando usar)
Use for-comprehensions para 2+ operações dependentes—são mais legíveis que flatMaps aninhados. Para uma única transformação, map é mais claro. Para aplainar, flatMap diretamente. Para efeitos colaterais (sem resultado), use for sem yield. For-comprehensions brilham quando cada passo depende do anterior (encadeamento monádico). Eles fazem código assíncrono/de tratamento de erros ler sequencialmente. Evite fors profundamente aninhados (>5 níveis)—extraia helpers para legibilidade.
// For: sequential, dependent operations
val result = for {
user <- fetchUser(id)
profile <- fetchProfile(user.id)
avatar <- fetchAvatar(profile.avatarId)
} yield avatar
// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
fetchProfile(user.id).flatMap(profile =>
fetchAvatar(profile.avatarId)))
// Map: single transformation (no chaining)
users.map(_.name) // simple, use map
// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts) // List[Post]
// For without yield: side effects
for (user <- users) {
saveToDb(user)
sendEmail(user)
}
// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yieldScala 3: Given e Using
Instâncias Given (substituindo implicit val)
Scala 3 substitui implicit val/def por 'given'. givens são mais claros e explícitos. Givens anônimos (given Type = ...) têm nomes gerados pelo compilador. 'given T with' define instâncias com múltiplos métodos. Givens condicionais (given [A: Ordering]: Ordering[List[A]]) substituem implicit defs. givens em companion objects estão em implicit scope automaticamente, assim como no Scala 2. A mudança de palavra-chave reduz a sobrecarga de 'implicit' (que significava 4 coisas no Scala 2).
// Scala 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global
// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global
// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global
// Given with 'with' for complex types
given Show[Int] with
def show(a: Int): String = a.toString
// Given in companion object (automatic scope)
object MyType:
given Ordering[MyType] = Ordering.by(_.id)
// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
def compare(a: List[A], b: List[A]): Int =
a.zip(b).find((x, y) => x != y) match
case Some((x, y)) => summon[Ordering[A]].compare(x, y)
case None => a.length - b.lengthCláusulas Using (substituindo implicit params)
Scala 3 substitui parâmetros implícitos por 'using'. Isso separa os dois significados de 'implicit' (parâmetros vs conversões). cláusulas using são passadas explicitamente com a palavra-chave 'using'. Context bounds [A: T] permanecem os mesmos. summon[T] substitui implicitly[T] (nome mais claro). A palavra-chave using torna óbvio nos call sites quando você está fornecendo um contexto. Isso é uma mudança puramente sintática—a semântica é a mesma de parâmetros implícitos.
// Scala 2: implicit parameter
// def process[A](data: List[A])(implicit ec: ExecutionContext): Unit
// Scala 3: using
def process[A](data: List[A])(using ec: ExecutionContext): Unit =
data.foreach(println)
// Context bound (unchanged)
def sort[A: Ordering](list: List[A]): List[A] = list.sorted
// Multiple using clauses
def log(msg: String)(using level: Level, logger: Logger): Unit =
logger.log(level, msg)
// summon replaces implicitly
def max[A: Ordering](a: A, b: A): A =
val ord = summon[Ordering[A]]
if ord.gt(a, b) then a else b
// Provide explicitly with 'using'
process(data)(using myEC)
log("hello")(using Level.INFO, myLogger)Extension Methods (Scala 3)
Scala 3 substitui implicit classes por 'extension'—mais claro e focado. extension (self: T) define métodos em T. Extensions podem ser genéricas (extension [A]). Combinadas com using, fornecem syntax de type class (42.show). Extensions são apenas adições de métodos—não criam objetos wrapper (zero overhead com AnyVal no Scala 2, nativo no Scala 3). Esta é a maneira idiomática de adicionar métodos a tipos existentes no Scala 3.
// Scala 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
// def squared: Int = self * self
// }
// Scala 3: extension
extension (self: Int)
def squared: Int = self * self
def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
5.squared // 25
3.times { println("hi") }
// Extension on generic type
extension [A](self: List[A])
def takeWhileInclusive(p: A => Boolean): List[A] = ???
// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)
42.show // uses given Show[Int]
// Multiple extensions in one block
extension (s: String)
def isBlank: Boolean = s.trim.isEmpty
def words: List[String] = s.split(" ").toListEnums e ADTs (Scala 3)
Enums do Scala 3 substituem sealed trait + case objects para ADTs. São mais concisos e suportam parâmetros, campos e métodos. Cases de enum podem ter parâmetros (como case classes). Pattern matching é verificado por exaustividade. Enums podem ser genéricos (Option[A]). Isso unifica enums e ADTs em um constructo. Para hierarquias abertas (extensíveis), use sealed trait + case classes ainda. Para enumerações/ADTs fechados, enum é mais limpo.
// Scala 3 enum (replaces sealed trait + case objects)
enum Color:
case Red, Green, Blue
enum HttpStatus(val code: Int):
case Ok extends HttpStatus(200)
case NotFound extends HttpStatus(404)
case Error extends HttpStatus(500)
// Pattern matching (exhaustive)
def describe(c: Color): String = c match
case Color.Red => "red"
case Color.Green => "green"
case Color.Blue => "blue"
// Access fields
HttpStatus.Ok.code // 200
// Parameterized enum cases
enum Option[+A]:
case Some(value: A)
case None
// ADT with methods
enum Tree[+A]:
case Leaf(value: A)
case Node(left: Tree[A], right: Tree[A])
def size: Int = this match
case Leaf(_) => 1
case Node(l, r) => l.size + r.size + 1Definições Top-Level e Indentação
Scala 3 permite definições top-level—sem necessidade de envolver tudo em um object. Isso simplifica a estrutura de arquivos (como Python/Go). A nova sintaxe suporta tanto chaves quanto indentação significativa (opcional). if/then substitui if/else com parênteses. match pode ser usado como uma expressão sem chaves. Essas mudanças tornam o Scala 3 mais acessível enquanto mantém compatibilidade com versões anteriores. Você pode misturar estilos—use chaves onde adicionam clareza, indentação onde reduz ruído.
// Scala 3: top-level definitions (no class wrapper needed)
// File: MyMath.scala
def add(a: Int, b: Int): Int = a + b // top-level function
val Pi: Double = 3.14159 // top-level val
type Id = Long // top-level type alias
given Show[Int] = (a: Int) => a.toString // top-level given
extension (i: Int) def squared: Int = i * i // top-level extension
// Indentation-based syntax (optional, braces still work)
def factorial(n: Int): Int =
if n <= 1 then 1
else n * factorial(n - 1)
// Or with braces:
def factorial2(n: Int): Int = {
if (n <= 1) 1
else n * factorial2(n - 1)
}
// if/then, match/case without braces
val sign = if x > 0 then 1 else -1
val desc = x match
case 0 => "zero"
case _ => "nonzero"Extension Methods e Syntax
Implicit Classes (Scala 2)
implicit class (Scala 2.10+) adiciona extension methods a tipos existentes. Estender AnyVal torna-a zero-allocation (o compilador apaga o wrapper). A classe deve receber um único parâmetro de construtor (o tipo sendo estendido). Métodos na implicit class tornam-se disponíveis no tipo estendido. É assim que Scala enriquece Int, String, etc. Coloque implicit classes em um package object ou utility object para compartilhamento. No Scala 3, use 'extension' em vez.
// Scala 2: implicit class for extension methods
import scala.language.implicitConversions
implicit class RichString(val s: String) extends AnyVal {
def wordCount: Int = s.split("\\s+").length
def slug: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-")
def encrypt(key: Int): String = s.map(c => (c + key).toChar)
}
"Hello World".wordCount // 2
"My Blog Post!".slug // "my-blog-post-"
"abc".encrypt(1) // "bcd"
// AnyVal avoids allocation (zero overhead)
// implicit class must be in a trait, class, or object
// For generic extensions
implicit class RichList[A](val list: List[A]) extends AnyVal {
def middle: Option[A] = list.lift(list.length / 2)
}
List(1, 2, 3, 4, 5).middle // Some(3)Extension Methods na Prática
Extension methods são a maneira padrão de adicionar utilitários a coleções e outros tipos. distinctBy, chunked, tap são adições comuns. Os operadores === e =!= (via type class Eq) fornecem igualdade type-safe (diferente de == que permite cross-type). Agrupe extensões em um object e importe onde necessário. Isso mantém os tipos centrais limpos enquanto permite métodos específicos de domínio. A biblioteca de coleções do Scala usa esse padrão extensivamente.
// Common pattern: add utility methods to collections
object CollectionExtensions {
implicit class RichSeq[A](val seq: Seq[A]) extends AnyVal {
def chunked(size: Int): Seq[Seq[A]] =
seq.grouped(size).toSeq
def distinctBy[B](f: A => B): Seq[A] =
seq.groupBy(f).values.map(_.head).toSeq
def tap(f: A => Unit): Seq[A] = {
seq.foreach(f)
seq
}
}
}
import CollectionExtensions._
List(1, 2, 3, 4, 5).chunked(2) // List(List(1,2), List(3,4), List(5))
List("aa", "bb", "ab").distinctBy(_.head) // List("aa", "ab")
List(1, 2, 3).tap(println) // prints 1,2,3, returns List(1,2,3)
// Type class syntax via extension
implicit class EqOps[A](val a: A) extends AnyVal {
def ===(b: A)(implicit eq: Eq[A]): Boolean = eq.eqv(a, b)
def =!=(b: A)(implicit eq: Eq[A]): Boolean = !eq.eqv(a, b)
}Conversões Implícitas (Use com Cuidado)
Conversões implícitas convertem automaticamente tipos, mas são perigosas—o comportamento do código torna-se não óbvio. Scala 2.10+ exige opt-in explícito (scala.language.implicitConversions). Prefira extension methods (que adicionam métodos sem mudar tipos) em vez de conversões (que mudam tipos). Usos legítimos: interop com Java (converter entre tipos de coleção), construção de DSL. O compilador avisa sobre conversões implícitas—trate esses avisos a sério. No Scala 3, given Conversion[T, U] é o mecanismo explícito.
import scala.language.implicitConversions
// Implicit conversion between types
implicit def stringToInt(s: String): Int = s.toInt
val n: Int = "42" // stringToInt("42")
// Dangerous: can cause surprising behavior
implicit def intToBoolean(n: Int): Boolean = n != 0
if (1) println("yes") // works! (intToBoolean(1))
// Safer: use extension methods instead
extension (n: Int) def toBool: Boolean = n != 0
if (1.toBool) println("yes")
// When implicit conversions are appropriate:
// 1. Java interop (java.util.List <-> scala.List)
implicit def javaListToScala[A](jl: java.util.List[A]): List[A] =
import scala.jdk.CollectionConverters._
jl.asScala.toList
// 2. Backward compatibility layers
// 3. DSL construction (use sparingly)
// Enable per-file: import scala.language.implicitConversionsProgramação em Nível de Tipo
Programação em nível de tipo codifica informações em tipos que o compilador verifica. Phantom types (parâmetros de tipo não usados) rastreiam estado (Open/Closed) prevenindo uso indevido (não pode ler um arquivo fechado). Peano naturals representam números como tipos. Isso permite garantias de correção em tempo de compilação—bugs tornam-se erros de compilação. Usado em state machines, unidades de medida, vetores dimensionados. Poderoso, mas complexo—use quando a segurança vale a complexidade de tipo. Bibliotecas como shapeless (Scala 2) permitem programação avançada em nível de tipo.
// Phantom types: track state at type level
sealed trait State
trait Open extends State
trait Closed extends State
class File[S <: State] private (val path: String)
object File {
def open(path: String): File[Open] = new File[Open](path)
}
def close[S <: Open](f: File[S]): File[Closed] =
new File[Closed](f.path) // unsafe cast internally
def read[S <: Open](f: File[S]): String = "data"
// def read[S <: Closed](f: File[S]): String // compile error
val f = File.open("test.txt")
read(f) // OK: f is File[Open]
val closed = close(f)
// read(closed) // COMPILE ERROR: closed is File[Closed]
// Type-level natural numbers (Peano)
sealed trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat
// Compile-time list length check
type _0 = Zero
type _1 = Succ[_0]
type _2 = Succ[_1]Opaque Types (Scala 3)
Opaque types (Scala 3) criam newtypes zero-overhead—diferente de value classes (AnyVal), eles nunca fazem boxing e são verdadeiramente apenas o tipo subjacente em runtime. A distinção de tipo existe apenas em tempo de compilação, prevenindo confusões (Celsius vs Fahrenheit, UserId vs Long). Dentro do object definidor, o tipo e seu tipo subjacente são intercambiáveis; fora, são distintos. Este é o padrão 'newtype' do Haskell—tipos orientados a domínio sem custo de runtime. Use para IDs, unidades e primitivas de domínio.
// Scala 2: value classes for zero-overhead wrappers
// case class UserId(value: Long) extends AnyVal
// Scala 3: opaque types (true zero overhead, no boxing)
object Types:
type UserId = Long
object UserId:
def apply(value: Long): UserId = value
type Email = String
object Email:
def apply(s: String): Email =
require(s.contains("@"), "invalid email")
s
import Types.*
val id: UserId = UserId(42) // just a Long at runtime
val email: Email = Email("[email protected]")
// UserId and Long are NOT interchangeable outside the object
// def wrong(x: Long): UserId = x // ERROR
// But inside the object, they're the same
// Newtype pattern: domain types without overhead
type Celsius = Double
type Fahrenheit = Double
// Prevents mixing up Celsius and Fahrenheit
def toF(c: Celsius): Fahrenheit = c * 9 / 5 + 32Testes (ScalaTest e ScalaCheck)
Estilos do ScalaTest
ScalaTest oferece múltiplos estilos. FunSuite é o mais simples (test("name") { ... }). FlatSpec é estilo BDD ("A Stack" should "..."). Matchers fornece asserções legíveis (shouldBe, should contain, should throw). Escolha um estilo por projeto para consistência. FunSuite é popular para testes de unidade; FlatSpec para testes focados em comportamento. Todos os estilos suportam os mesmos matchers e hooks de lifecycle. O estilo afeta a sintaxe, não as capacidades.
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers
// FunSuite: simple test functions
class MyTest extends AnyFunSuite with Matchers {
test("addition works") {
1 + 1 should be (2)
1 + 1 shouldBe 2
List(1, 2, 3) should contain (2)
}
test("string operations") {
"hello".length shouldBe 5
"hello" should startWith ("he")
}
}
// FlatSpec: BDD-style
import org.scalatest.flatspec.AnyFlatSpec
class StackSpec extends AnyFlatSpec with Matchers {
"A Stack" should "pop values in LIFO order" in {
val stack = Stack(1, 2, 3)
stack.pop() shouldBe 3
}
it should "throw on empty pop" in {
val stack = Stack()
a [NoSuchElementException] shouldBe thrownBy(stack.pop())
}
}
// WordSpec, FreeSpec, PropSpec also availableAsserções e Matchers
Matchers fornecem asserções expressivas. shouldBe/should be para igualdade. contain, have size, have key para coleções. startWith/endWith/include para strings. thrownBy para exceções. Matchers customizados (be >, be <=) para comparações. A DSL 'should' lê como inglês, tornando os testes auto-documentáveis. Para asserções complexas, use matchers customizados ou assert(condition) simples. Evite encadear matchers em excesso—legibilidade sobre esperteza.
import org.scalatest.matchers.should.Matchers._
// Equality
result shouldBe 42
result should be (42)
result should equal (42)
// Collections
list should contain (5)
list should not contain (0)
list shouldBe empty
list should have size 5
list should contain inOrder (1, 2, 3)
map should contain key ("name")
map should contain value ("Alice")
// Strings
s should startWith ("Hello")
s should endWith ("world")
s should include ("lo")
s should fullyMatch regex "H.*d".r
// Exceptions
a [IOException] should be thrownBy riskyOp()
the [IOException] thrownBy riskyOp() should have message "fail"
// Custom matchers
result should be > 0
result should be <= 100
// Type checks
result shouldBe a [List[_]]
result should be an [IllegalArgumentException]BeforeAndAfter e Fixtures
BeforeAndAfterEach executa setup/teardown ao redor de cada teste. Sobrescreva beforeEach/afterEach. Para gerenciamento de recursos, o loan pattern (withDb { conn => ... }) é mais limpo—garante limpeza via try/finally e torna o recurso explícito. ScalaTest também suporta contextos de fixture (FixtureContext) e fixtures compartilhados via traits. Prefira o loan pattern ou métodos de fixture em vez de estado beforeEach mutável—é mais funcional e evita bugs de estado compartilhado entre testes.
import org.scalatest.BeforeAndAfterEach
import org.scalatest.funsuite.AnyFunSuite
class DbTest extends AnyFunSuite with BeforeAndAfterEach {
var conn: Connection = _
override def beforeEach(): Unit = {
conn = DriverManager.getConnection("jdbc:h2:mem:test")
conn.execute("CREATE TABLE users (id INT, name VARCHAR)")
}
override def afterEach(): Unit = {
conn.close()
}
test("insert works") {
conn.execute("INSERT INTO users VALUES (1, 'Alice')")
val count = conn.query("SELECT COUNT(*) FROM users")
count shouldBe 1
}
}
// Fixture via loan pattern
class FixtureTest extends AnyFunSuite {
def withDb(test: Connection => Unit): Unit = {
val conn = DriverManager.getConnection("jdbc:h2:mem:test")
try test(conn) finally conn.close()
}
test("query works") {
withDb { conn =>
conn.execute("INSERT ...")
// assertions
}
}
}Property-Based Testing (ScalaCheck)
ScalaCheck gera entradas de teste aleatórias, testando propriedades em muitos casos. forAll executa a propriedade com 100 entradas aleatórias por padrão. whenever filtra entradas. Tipos Gen customizados restringem dados gerados (Gen.choose, Gen.nonEmptyListOf). Isso captura casos extremos que você perderia com testes baseados em exemplo (listas vazias, números negativos, valores grandes). Testes table-driven (Table) são para casos específicos. Property-based testing é poderoso para funções puras e transformações de dados—defina o que deve ser sempre verdadeiro.
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Prop.forAll
class ListSpec extends AnyFunSuite with ScalaCheckPropertyChecks {
// Property: reversing twice = identity
test("reverse twice is identity") {
forAll { (xs: List[Int]) =>
xs.reverse.reverse shouldBe xs
}
}
// Property with conditions
test("head of sorted list is min") {
forAll { (xs: List[Int]) =>
whenever(xs.nonEmpty) {
xs.sorted.head shouldBe xs.min
}
}
}
// Custom generators
import org.scalacheck.Gen
val smallInt = Gen.choose(1, 100)
val nonEmptyList = Gen.nonEmptyListOf(smallInt)
test("custom generator") {
forAll(nonEmptyList) { (xs: List[Int]) =>
xs should not be empty
xs.forall(_ >= 1) shouldBe true
}
}
// Table-driven tests
test("addition table") {
val cases = Table(
("a", "b", "sum"),
(1, 2, 3),
(10, 20, 30),
(-1, 1, 0)
)
forAll(cases) { (a, b, sum) =>
a + b shouldBe sum
}
}
}Mocking e Test Doubles
Mockito (via ScalaTestPlus) cria test doubles. mock[T] cria um mock; when(...).thenReturn(...) faz stub de métodos; verify verifica interações. Use mocks para isolar a unidade em teste de dependências (bancos de dados, APIs). Argument matchers (argThat) verificam argumentos específicos. Não faça mock em excesso—se você está mockando tudo, teste a integração real em vez. Prefira fakes (implementações em memória) em vez de mocks para dependências complexas—são mais robustos e legíveis.
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._
class UserServiceSpec extends AnyFunSuite with MockitoSugar {
test("getUser returns user from repo") {
// Create mock
val repo = mock[UserRepository]
val user = User(1, "Alice")
// Stub: when X then Y
when(repo.findById(1)).thenReturn(Some(user))
val service = new UserService(repo)
val result = service.getUser(1)
result shouldBe Some(user)
// Verify: was X called?
verify(repo).findById(1)
verify(repo, never()).findById(2)
}
test("createUser saves to repo") {
val repo = mock[UserRepository]
val service = new UserService(repo)
service.createUser("Bob")
// Verify with argument matcher
verify(repo).save(argThat((u: User) => u.name == "Bob"))
}
}
// Stubbing exceptions
when(repo.findById(99)).thenThrow(new RuntimeException("not found"))Funções de Ordem Superior e FP
Funções como Valores
Funções são valores em Scala—você pode armazená-las, passá-las e retorná-las. (A, B) => C é o tipo de função. Eta-expansion (multiply _) converte métodos em valores de função. Funções de ordem superior (recebem/retornam funções) permitem abstrações poderosas: map, filter, reduce são HOFs. Composição de funções (andThen, compose) constrói pipelines. Currying (adder(5) retorna uma função) aplicação parcial. Esta é a fundação da programação funcional em Scala.
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x
// Apply
add(2, 3) // 5
square(4) // 16
// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _ // or just 'multiply' in Scala 3
mul(3, 4) // 12
// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2) // 16
// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10) // 15
// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g // f then g: (x+1)*2
val h2 = f compose g // g then f: (x*2)+1
h(3) // 8
h2(3) // 7Currying e Aplicação Parcial
Currying divide uma função em múltiplas listas de parâmetros, permitindo aplicação parcial (fixe alguns argumentos, obtenha uma função para o resto). Isso auxilia a inferência de tipos (parâmetros anteriores restringem posteriores) e cria funções configuráveis (withDb(config)). O placeholder _ aplica parcialmente: sum(1, _) cria uma função. Múltiplas listas de parâmetros são o mecanismo de currying do Scala. Use currying quando alguns argumentos são 'configuração' e outros são 'dados'.
// Curried function: multiple parameter lists
def add(a: Int)(b: Int): Int = a + b
val add5: Int => Int = add(5) // partial application
add5(10) // 15
// Multiple parameter lists for type inference
def map[A, B](list: List[A])(f: A => B): List[B] =
list.map(f)
map(List(1, 2, 3))(x => x * 2) // A, B inferred from first list
// Curried form (Function types)
val curriedAdd: Int => Int => Int = a => b => a + b
curriedAdd(5)(10) // 15
// Uncurrying
def uncurriedAdd(a: Int, b: Int): Int = a + b
// Practical: configuration via currying
def withDb(config: DbConfig)(f: Connection => Result): Result = ???
val withMyDb = withDb(myConfig) _
withMyDb { conn => /* ... */ }
// Partial application with placeholder
val sum: (Int, Int) => Int = _ + _
val addOne: Int => Int = sum(1, _)Funções Puras e Transparência Referencial
Funções puras sempre retornam a mesma saída para a mesma entrada e não têm efeitos colaterais. São fáceis de testar, raciocinar, paralelizar e compor. Transparência referencial significa que você pode substituir uma chamada de função por seu resultado sem mudar o comportamento. Scala não impõe pureza, mas bibliotecas como Cats Effect (IO monad) permitem isolar efeitos colaterais. Core puro + IO nas bordas é uma arquitetura FP comum: a lógica de negócios é pura, E/S é envolvida em IO.
// Pure function: same input → same output, no side effects
def pureAdd(a: Int, b: Int): Int = a + b // pure
// Impure: depends on external state
var counter = 0
def impureAdd(a: Int): Int = { counter += 1; a + counter } // impure
// Impure: side effect
def impurePrint(a: Int): Int = { println(a); a } // side effect
// Referential transparency: can replace call with result
val x = pureAdd(2, 3)
val y = x + x // same as pureAdd(2, 3) + pureAdd(2, 3)
// Benefits of purity:
// 1. Easy to test (no setup/teardown)
// 2. Easy to reason about (no hidden state)
// 3. Parallelizable (no shared state)
// 4. Memoizable (cache results)
// 5. Composable (predictable)
// IO monad for side effects (Cats Effect)
import cats.effect.IO
val program: IO[Unit] = IO.println("hello")
val mapped: IO[String] = IO.pure("world").map(_.toUpperCase)
// Side effects captured in IO, run at edge of programDados Imutáveis e Coleções Persistentes
Estruturas de dados imutáveis retornam novas cópias em modificação, compartilhando estrutura internamente para eficiência (persistent data structures). As coleções do Scala são imutáveis por padrão. case class copy cria cópias modificadas. Para atualizações profundas, bibliotecas Lens (Monocle) fornecem accessors composáveis. Imutabilidade elimina classes inteiras de bugs (race conditions, mutações inesperadas) e torna o código mais fácil de raciocinar. O custo de desempenho é frequentemente aceitável devido a structural sharing (O(log n) não O(n)).
// Immutable: operations return new collections
val list1 = List(1, 2, 3)
val list2 = list1 :+ 4 // List(1, 2, 3, 4)
// list1 is unchanged: List(1, 2, 3)
// Persistent data structures: share structure (efficient)
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3) // shares structure with map1
// O(log n) due to structural sharing, not O(n) copy
// Case classes: copy for modification
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31) // new instance, alice unchanged
// Lens (Monocle library) for nested updates
import monocle.macros.GenLens
val ageLens = GenLens[User](_.age)
val updated = ageLens.modify(_ + 1)(alice) // User("Alice", 31)
// Benefits:
// - No bugs from shared mutation
// - Easy to reason about (values don't change)
// - Free concurrency (no locks needed)
// - Undo/redo trivial (keep old versions)Recursão e Tail Calls
Tail recursion (onde a chamada recursiva é a última operação) é otimizada pelo compilador Scala para um loop—sem crescimento de pilha. @tailrec verifica isso em tempo de compilação. O padrão accumulator (passando resultado acumulado) converte recursão não-tail em tail recursion. foldLeft/foldRight encapsulam padrões comuns de recursão. Para recursão não-tail profunda, use trampolines (Cats) ou reescreva com folds. Prefira folds em vez de recursão explícita para clareza—são tail-recursive e idiomáticos.
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000) // StackOverflowError!
// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000) // works (no stack overflow)
// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive
// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap
// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally
// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
list match
case Nil => acc
case head :: tail => reverse(tail, head :: acc)Case Classes e ADTs
Noções Básicas de Case Class
Case classes são a principal ferramenta de modelagem de dados do Scala. O compilador gera equals, hashCode, toString, copy, apply, unapply (para pattern matching) e métodos accessor. São imutáveis por padrão (campos val). Use case classes para DTOs, value objects, mensagens e entidades de domínio (quando imutáveis). O apply do companion object permite construir sem 'new'. copy habilita atualizações não-destrutivas. Case classes são a fundação de ADTs (Algebraic Data Types).
// Case class: immutable data holder with boilerplate generated
case class Person(name: String, age: Int)
val alice = Person("Alice", 30)
val bob = Person("Bob", 25)
// Auto-generated methods:
alice.toString // "Person(Alice,30)"
alice == Person("Alice", 30) // true (structural equality)
alice.hashCode // based on fields
// Pattern matching
alice match
case Person(name, age) => s"$name is $age"
// Copy with modifications
val older = alice.copy(age = 31) // Person("Alice", 31)
// Companion object with apply (no 'new' needed)
val p = Person("Charlie", 40) // Person.apply called
// Fields accessed directly
alice.name // "Alice"
alice.age // 30
// Case class with default values
case class Point(x: Double = 0, y: Double = 0)
Point() // Point(0.0, 0.0)
Point(y = 5) // Point(0.0, 5.0)Sealed Traits e ADTs
ADTs (Algebraic Data Types) modelam dados como um conjunto fechado de cases (sealed trait + case classes/objects). 'sealed' significa que todos os subtipos estão no mesmo arquivo, habilitando pattern matching exaustivo—o compilador avisa se você perder um case. Adicionar um novo case mostra todos os lugares que precisam de atualização (refatoração segura). Case objects são singletons (sem parâmetros). Option, List, Either são todos ADTs. Esta é a maneira funcional de modelar domínios—cada variante é um case, o comportamento está no pattern matching.
// ADT: sealed trait + case classes/objects
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(a: Double, b: Double, c: Double) extends Shape
// Exhaustive pattern matching (compiler checks all cases)
def area(shape: Shape): Double = shape match
case Circle(r) => math.Pi * r * r
case Rectangle(w, h) => w * h
case Triangle(a, b, c) =>
val s = (a + b + c) / 2
math.sqrt(s * (s-a) * (s-b) * (s-c))
// Adding a new case: compiler warns about non-exhaustive matches
case class Square(side: Double) extends Shape
// area now needs a Square case!
// Case objects for singletons (no parameters)
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status
// Option and List are ADTs:
// sealed trait Option[+A]
// case class Some[A](value: A) extends Option[A]
// case object None extends Option[Nothing]Recursos de Case Class
Case classes podem ter métodos, apply customizado (para validação), implementar traits e ser genéricas. apply customizado no companion object pode validar antes da construção (torne estados ilegais irrepresentáveis). Case classes podem implementar traits para polimorfismo. O construtor privado (case class Email private) força usar o apply do companion para validação. Isso combina modelagem de dados com encapsulamento—construtores que não podem falhar silenciosamente.
// Case class with methods
case class Vec2(x: Double, y: Double) {
def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
def magnitude: Double = math.sqrt(x*x + y*y)
def dot(other: Vec2): Double = x*other.x + y*other.y
}
Vec2(1, 2) + Vec2(3, 4) // Vec2(4.0, 6.0)
Vec2(3, 4).magnitude // 5.0
// Case class with custom apply (validation)
case class Email private (value: String)
object Email {
def apply(value: String): Email = {
require(value.contains("@"), "invalid email")
new Email(value) // bypass public apply
}
}
// Email("notanemail") // IllegalArgumentException
Email("[email protected]") // works
// Case class implementing trait
trait Jsonable {
def toJson: String
}
case class User(name: String, age: Int) extends Jsonable {
def toJson: String = s"""{"name":"$name","age":$age}"""
}
// Case class with type parameters
case class Box[A](value: A) {
def map[B](f: A => B): Box[B] = Box(f(value))
}Pattern Matching Profundo
Pattern matching é a poderosa ferramenta de destructuring do Scala. Suporta: padrões literais (0, "hello"), padrões de tipo (s: String), padrões de case class (User(name, age)), guards (if condition), padrões aninhados e binding (@). O compilador verifica exaustividade para tipos sealed. Padrões são tentados de cima para baixo. Use @ para vincular o valor inteiro enquanto extrai partes. Pattern matching substitui cadeias if-else, verificações de tipo e destructuring por uma sintaxe unificada e legível.
// Various pattern types
val x: Any = (1, "hello")
x match
case (a: Int, b: String) => s"int $a, string $b"
case (a, b) => s"pair: $a, $b"
case _ => "other"
// Guards
def classify(n: Int): String = n match
case n if n < 0 => "negative"
case 0 => "zero"
case n if n % 2 == 0 => "even positive"
case _ => "odd positive"
// Case class patterns (nested)
case class Point(x: Int, y: Int)
case class Shape(center: Point, size: Int)
def isOrigin(s: Shape): Boolean = s match
case Shape(Point(0, 0), _) => true
case _ => false
// Named patterns (bind the whole while extracting)
case class User(name: String, age: Int)
def describe(u: User): String = u match
case u @ User(name, age) if age < 18 => s"$name is a minor"
case User(name, _) => s"$name is an adult"
// Type patterns
def handle(x: Any): String = x match
case s: String => s"string: $s"
case n: Int => s"int: $n"
case list: List[_] => s"list of size ${list.size}"
case None => "none"
case _ => "unknown"Extractors e Padrões Customizados
Extractors (unapply) habilitam pattern matching em qualquer tipo, não apenas case classes. Isso desacopla o padrão de matching da representação de dados. Regexes são extractors (grupos tornam-se bindings). Você pode escrever extractors para tipos externos (JSON, URLs) sem modificá-los. unapply retorna Option[(T1, T2, ...)] para extração, ou Boolean para matching simples. Extractors tornam pattern matching extensível a qualquer fonte de dados. Case classes geram unapply automaticamente; extractors customizados adicionam matching para tipos não-case-class.
// Custom extractor via unapply
object Email {
def unapply(str: String): Option[(String, String)] = {
val parts = str.split("@")
if (parts.length == 2) Some((parts(0), parts(1)))
else None
}
}
// Use in pattern matching
"[email protected]" match
case Email(user, domain) => s"user: $user, domain: $domain"
case _ => "not an email"
// Boolean extractor (no extracted values)
object Even {
def unapply(n: Int): Boolean = n % 2 == 0
}
5 match
case Even() => "even"
case _ => "odd"
// Extractor with variable arity
object Pair {
def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t._1, t._2)
}
// Regex as extractor
val Date = "(\\d{4})-(\\d{2})-(\\d{2})".r
"2024-01-15" match
case Date(year, month, day) => s"$year/$month/$day"
case _ => "not a date"
// Practical: parse without case classes
object Json {
def unapply(s: String): Option[Any] =
scala.util.Try(ujson.read(s)).toOption
}Coleções Profundo
Coleções Imutáveis
Coleções Scala são imutáveis por padrão. List é uma lista encadeada (O(n) acesso aleatório). Vector é uma árvore com acesso O(log n). Map e Set são baseados em hash. Operações retornam novas coleções, compartilhando estrutura para eficiência.
val list = List(1, 2, 3)
val vector = Vector(1, 2, 3) // Fast random access
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
// All immutable: operations return new collections
val updated = map + ("c" -> 3) // New mapOperações de Coleção
map transforma elementos. filter seleciona. reduce combina. grouped divide em chunks. flatten mescla coleções aninhadas. flatMap mapeia e aplaina. Todos retornam novas coleções. Coleções lazy usam .view ou .iterator para eficiência.
val nums = (1 to 10).toList
val doubled = nums.map(_ * 2)
val evens = nums.filter(_ % 2 == 0)
val sum = nums.reduce(_ + _)
val grouped = nums.grouped(3).toList // List(List(1,2,3), List(4,5,6), ...)
val flat = List(List(1,2), List(3,4)).flatten // List(1,2,3,4)Pattern Matching em Coleções
Pattern matching funciona em coleções. :: (cons) corresponde head e tail. List(a, b, c) corresponde exatamente 3 elementos. Nil corresponde lista vazia. _ é wildcard. Útil para parsing e destructuring. Matching exaustivo previne bugs.
val list = List(1, 2, 3)
list match {
case head :: tail => println(s"Head: $head")
case Nil => println("Empty")
}
List(1, 2, 3) match {
case List(a, b, c) => println(s"$a, $b, $c")
case _ => println("Other")
}Coleções Mutáveis
Coleções mutáveis (ArrayBuffer, mutable.Set, mutable.Map) modificam in-place. Mais rápidas para atualizações frequentes, mas não thread-safe. Use quando o desempenho importa e a imutabilidade não é necessária. Converta para imutável com .toMap, .toSet.
import scala.collection.mutable
val buffer = mutable.ArrayBuffer(1, 2, 3)
buffer += 4 // Add element
buffer -= 2 // Remove element
buffer(0) = 10 // Update
val mset = mutable.Set(1, 2, 3)
val mmap = mutable.Map("a" -> 1)
mmap("b") = 2Coleções Lazy
view cria uma lazy view: operações são adiadas até forçadas. Útil para operações encadeadas em coleções grandes. LazyList (Scala 2.13+) é uma sequência lazy. Sequências infinitas são possíveis com lazy evaluation. Force com .toList, .toArray.
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head // Only computes first
val result = lazyView.take(10).toList // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)Implicits
Parâmetros Implícitos
Parâmetros implícitos são passados automaticamente quando no escopo. O compilador pesquisa um valor implícito correspondente. Usado para configuração, type classes e contexto. Pode ser sobrescrito explicitamente. Múltiplos implicits devem ter tipos distintos.
def connect(url: String)(implicit timeout: Int): Unit = {
println(s"Connecting to $url with timeout $timeout")
}
implicit val defaultTimeout: Int = 5000
connect("http://example.com") // Uses 5000
connect("http://example.com")(3000) // Explicit overrideConversões Implícitas
Conversões implícitas convertem tipos automaticamente. Podem ser perigosas (conversões inesperadas). implicit class adiciona extension methods. Scala 3 usa given/using e extension para clareza. Prefira extension methods em vez de conversões brutais.
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42 // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
def squared: Int = n * n
}
5.squared // 25Type Classes
Type classes são um padrão para polimorfismo ad-hoc. Uma trait define comportamento, instâncias fornecem implementações para tipos específicos. Resolução implícita encontra a instância. Mais flexível que herança. Comum em Cats e Shapeless.
trait Show[A] {
def show(a: A): String
}
object Show {
implicit val intShow: Show[Int] = (a: Int) => a.toString
implicit val stringShow: Show[String] = (a: String) => a
def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42) // "42"
Show("hello") // "hello"Context Bounds
Context bounds [A: TypeClass] são syntactic sugar para parâmetros implícitos. A instância da type class é disponível via implicitly. Sintaxe mais limpa para restrições de type class. Comum com Ordering, Numeric e type classes customizadas.
def max[A: Ordering](a: A, b: A): A = {
val ord = implicitly[Ordering[A]]
if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
if (ord.gt(a, b)) a else bGiven/Using (Scala 3)
Scala 3 substitui implicits por given/using para clareza. given define uma instância, using declara um parâmetro. extension substitui implicit class. Mais explícito e legível. Ferramentas de migração convertem implicits do Scala 2.
// Scala 3 syntax
given defaultTimeout: Int = 5000
def connect(url: String)(using timeout: Int): Unit =
println(s"Connecting with $timeout")
connect("http://example.com") // Uses given
// Extension methods
extension (n: Int)
def squared: Int = n * nConcorrência
Future
Future representa uma computação assíncrona. onComplete trata conclusão. map/flatMap encadeiam operações. ExecutionContext fornece threads. Futures são imutáveis e one-shot. Use for-comprehension para múltiplos futures.
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
Thread.sleep(1000)
42
}
f.onComplete {
case Success(v) => println(v)
case Failure(e) => println(e)
}
// Map/flatMap for chaining
val f2 = f.map(_ * 2).flatMap(x => Future(x + 1))for-comprehension com Futures
for-comprehension desuga para flatMap/map. Sequencial: cada passo espera o anterior. Para execução paralela, inicie futures antes do for. Muito mais legível que callbacks aninhados. Funciona com qualquer monad (Future, Option, List).
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
user <- getUser(1)
orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chainingFutures Paralelos
Iniciar futures antes do for-comprehension executa-os em paralelo. Future.sequence converte List[Future[T]] para Future[List[T]]. Future.traverse mapeia e sequencia em um passo. zip combina dois futures. Todos concluem quando o mais lento termina.
val f1 = Future { compute1() }
val f2 = Future { compute2() }
val combined: Future[(Int, Int)] = for {
r1 <- f1
r2 <- f2
} yield (r1, r2)
// Or: Future.sequence(List(f1, f2))
// Or: Future.traverse(list)(compute)Actors (Akka)
Akka actors encapsulam estado e comunicam-se via mensagens. Sem estado mutável compartilhado. Cada actor processa uma mensagem por vez. ! (tell) envia fire-and-forget. ? (ask) retorna um Future. Supervision trata falhas. Ideal para sistemas stateful concorrentes.
import akka.actor.*
class Counter extends Actor {
var count = 0
def receive = {
case "inc" => count += 1
case "get" => sender() ! count
}
}
val system = ActorSystem("mySystem")
val counter = system.actorOf(Props[Counter], "counter")
counter ! "inc"
counter ! "get"Cats Effect IO
Cats Effect IO é uma IO monad funcional pura. Referentially transparent: IO(println("x")) é um valor. Compõe com for-comprehension. Cancellable e resource-safe. unsafeRunSync executa na borda do seu programa. Alternativa a Future com melhor semântica.
import cats.effect.IO
val program: IO[Int] = for {
_ <- IO(println("Start"))
result <- IO.pure(42)
_ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellablePattern Matching Profundo
Case Classes
Case classes geram automaticamente equals, hashCode, toString e apply/unapply. Perfeitas para pattern matching. Pattern matching as destrutura. Imutáveis por padrão. Use para algebraic data types. copy() cria cópias modificadas.
case class Point(x: Int, y: Int)
val p = Point(1, 2)
p match {
case Point(0, 0) => "origin"
case Point(0, _) => "on y-axis"
case Point(x, 0) => s"on x-axis at $x"
case Point(x, y) => s"at ($x, $y)"
}Sealed Traits
sealed traits restringem subtipos ao mesmo arquivo. O compilador verifica exaustividade no pattern matching. Adicionar um novo subtipo causa avisos em todos os matches. Ideal para hierarquias de tipo fechadas. Combinado com case classes para ADTs.
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Square(s) => s * s
case Rectangle(w, h) => w * h
} // Compiler warns if not exhaustiveGuards e Extractors
Guards (if) adicionam condições a padrões. Extractors customizados (unapply) habilitam pattern matching em qualquer tipo. unapply retorna Option para indicar correspondência. Extractors desacoplam matching do tipo. Poderoso para DSLs e parsing.
def classify(n: Int): String = n match {
case x if x < 0 => "negative"
case 0 => "zero"
case x if x % 2 == 0 => "even"
case _ => "odd"
}
// Custom extractor
object Even {
def unapply(n: Int): Option[Int] =
if (n % 2 == 0) Some(n / 2) else None
}
4 match { case Even(half) => s"half is $half" }Partial Functions
Partial functions são definidas apenas para algumas entradas. isDefinedAt verifica. collect aplica apenas onde definido. Útil para callbacks e routing. orElse combina partial functions. lift converte para função total retornando Option.
val pf: PartialFunction[Int, String] = {
case 1 => "one"
case 2 => "two"
}
pf.isDefinedAt(1) // true
pf.isDefinedAt(3) // false
// Collect = filter + map
List(1, 2, 3, 1).collect(pf) // List("one", "two", "one")Pattern Matching de Tipos
Pattern matching funciona em tipos, mas type erasure afeta generics. List(a, b) corresponde a uma lista de 2 elementos. List(_*) corresponde a qualquer lista. Evite matching em tipos genéricos como List[Int] (apagado). Use type tags para informação de tipo em runtime.
def describe(x: Any): String = x match {
case i: Int => s"Int: $i"
case s: String => s"String: $s"
case List(a, b) => s"Two-element list: $a, $b"
case List(_*) => "List with elements"
case Some(v) => s"Some: $v"
case None => "None"
case _ => "Unknown"
}Programação Funcional
Funções de Ordem Superior
Funções de ordem superior recebem ou retornam funções. Currying divide funções multi-arg em cadeias de arg único. Aplicação parcial fixa alguns argumentos. Habilita composição e reuso de funções. _ cria uma função parcialmente aplicada.
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5) // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _ // Partial application
add5(3) // 8Option e Either
Option representa valores opcionais: Some ou None. Evita null. getOrElse fornece padrão. Either representa sucesso (Right) ou falha (Left). Melhor que exceções para erros esperados. Ambos são monads: map, flatMap, for-comprehension.
def find(id: Int): Option[String] =
if (id > 0) Some("Alice") else None
find(1).getOrElse("Unknown") // "Alice"
find(-1).getOrElse("Unknown") // "Unknown"
// Either for error handling
def parse(s: String): Either[String, Int] =
try Right(s.toInt)
catch { case _: Exception => Left(s"Not a number: $s") }Composição de Funções
compose encadeia funções da direita para esquerda (como matemática). andThen encadeia da esquerda para direita (mais legível). Ambas criam novas funções. Útil para construir pipelines. Funções são valores first-class em Scala.
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
// Compose (right to left)
val f = addOne compose double // double then addOne
f(3) // 7
// AndThen (left to right)
val g = addOne andThen double // addOne then double
g(3) // 8Recursão e Tail Recursion
Tail recursion é otimizada para um loop pelo compilador. A chamada recursiva deve ser a última operação. A annotation @tailrec verifica isso em tempo de compilação. O padrão accumulator carrega estado. Previne stack overflow para recursão profunda.
// Not tail-recursive: stack overflow for large n
def factorial(n: Int): Int =
if (n <= 1) 1 else n * factorial(n - 1)
// Tail-recursive: optimized to loop
import scala.annotation.tailrec
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
if (n <= 1) acc else factorial(n - 1, n * acc)Monads
Monads têm flatMap e unit (pure/pure). Elas encadeiam operações com contexto (Option: ausência, List: não-determinismo, Future: async). for-comprehension desuga para flatMap/map. Leis de Monad garantem composição correta. Cats fornece a type class Monad.
// Monad laws: left identity, right identity, associativity
// Option is a monad:
Some(5).flatMap(x => Some(x + 1)) // Some(6)
None.flatMap(x => Some(x + 1)) // None
// List is a monad:
List(1, 2).flatMap(x => List(x, x * 10)) // List(1, 10, 2, 20)
// for-comprehension is monadic sugar:
for {
x <- Some(5)
y <- Some(x + 1)
} yield y // Some(6)Armadilhas Comuns
Null vs Option
Scala tem null para compatibilidade com Java, mas é desencorajado. Option representa ausência explicitamente. Pattern matching força tratamento de None. Use .toOption em Try para exceções. Evite null em código Scala; reserve para interop com Java.
// BAD: null
def find(id: Int): String =
if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic ScalaVar vs Val
val é imutável (valor), var é mutável (variável). Prefira val para código mais seguro e previsível. Estado mutável complica raciocínio e concorrência. Use var apenas para desempenho local ou quando realmente necessário. Coleções são imutáveis por padrão.
var x = 1 // Mutable
x = 2 // OK
val y = 1 // Immutable
// y = 2 // Error
// Prefer val for immutability
// Use var only when necessary
// Mutable state causes bugs in concurrent codeIgualdade
Scala == chama equals (igualdade de valor), diferentemente do Java. eq verifica igualdade de referência. ne é a negação de eq. Sempre use == para comparação de valor. Case classes têm equals correto. Para classes customizadas, sobrescreva equals e hashCode.
val a = List(1, 2)
val b = List(1, 2)
a == b // true (value equality)
a eq b // false (reference equality)
// In Java: a == b compares references
// In Scala: == calls equals (value)
// Use eq for reference equality (rarely needed)Ambiguidade Implícita
Múltiplos implicits do mesmo tipo causam erros de ambiguidade. Mantenha o implicit scope limpo. Use tipos específicos (newtypes) para distinguir. given/using do Scala 3 é mais claro. Evite conversões implícitas; elas causam comportamento surpreendente.
implicit val s1: String = "hello"
implicit val s2: String = "world"
def greet(implicit s: String) = println(s)
greet // Error: ambiguous implicit values
// Fix: only one implicit in scope
// Or: pass explicitly
greet(s1)By-Name vs By-Value
Parâmetros by-name (=> T) são avaliados lazymente, a cada uso. Parâmetros by-value são avaliados uma vez antes da chamada. By-name habilita estruturas de controle customizadas (unless, while). Pode causar múltiplas avaliações; faça cache com lazy val se necessário.
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation()) // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation()) // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
if (!cond) bodyAkka Actors
Actor System
ActorSystem é o container para actors. actorOf cria actors com um nome. ! (tell) envia uma mensagem assincronamente. ? (ask) envia e retorna um Future. Actors são identificados por path. terminate desliga o sistema graciosamente.
import akka.actor.*
val system = ActorSystem("MySystem")
val actor = system.actorOf(Props[MyActor], "myActor")
actor ! "Hello" // Fire-and-forget
val future = actor ? "Query" // Ask pattern
system.terminate()Lifecycle do Actor
preStart executa na criação, postStop no término. preRestart/postRestart tratam supervision. O actor é parado antes do restart, depois iniciado fresh. Estado interno é perdido no restart. Use preStart para inicialização e postStop para limpeza.
class MyActor extends Actor {
override def preStart(): Unit = println("Starting")
override def postStop(): Unit = println("Stopped")
override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
println("Restarting")
super.preRestart(reason, message)
}
def receive = {
case "ping" => sender() ! "pong"
}
}Supervision
Supervision define como falhas são tratadas. Resume: continue com o mesmo estado. Restart: recrie com estado fresh. Stop: termine permanentemente. Escalate: deixe o pai tratar. OneForOneStrategy afeta apenas o filho falho. AllForOneStrategy afeta todos os filhos.
class Supervisor extends Actor {
override val supervisorStrategy = OneForOneStrategy() {
case _: ArithmeticException => Resume // Continue
case _: NullPointerException => Restart // Restart actor
case _: Exception => Stop // Stop actor
}
def receive = { case _ => }
}Routers
Routers distribuem mensagens para múltiplos actors. Pool cria e gerencia workers. Group usa actors existentes. Estratégias: RoundRobin, Random, SmallestMailbox, ConsistentHashing, ScatterGatherFirst. Routers melhoram throughput paralelizando trabalho.
// Pool router: creates workers
val router = system.actorOf(
RoundRobinPool(5).props(Props[Worker]),
"router"
)
router ! "work" // Distributed to one of 5 workers
// Group router: uses existing actors
val group = system.actorOf(
RoundRobinGroup(paths).props(),
"group"
)Persistência
PersistentActor salva eventos em um journal (event sourcing). Comandos são validados, depois persistidos como eventos. No restart, eventos são reproduzidos via receiveRecover. Estado é reconstruído a partir de eventos. Habilita recuperação de crashes. Use EventSourcedBehavior para Akka Typed.
class Counter extends PersistentActor {
var count = 0
override def persistenceId = "counter-1"
override def receiveCommand = {
case "inc" => persist(Incremented) { event =>
count += 1
}
}
override def receiveRecover = {
case Incremented => count += 1
}
}Snippets de Scala relacionados
Copy-paste ready code for common tasks.
Correspondência de Padrões
Desestruturar valores e corresponder casos em Scala.
Case Classes
Classes de dados imutáveis com equals/hashCode/toString gerados automaticamente.
Operações de Coleções
Operações funcionais de coleção: map, filter, fold, groupBy.
Traits e Mixins
Compor comportamentos usando traits com implementações padrão.
Futures e Async
Computação assíncrona com Future e ExecutionContext.
Implicits (Given/Using no Scala 3)
Derivação de type classes e passagem de contexto via implicits.
Akka Actors (Pekko)
Concorrência por passagem de mensagens com o modelo de atores.
Type Classes (estilo Cats)
Polimorfismo ad-hoc via type classes.
Was this helpful?