DevTools

Cheatsheet Scala

Linguagem multi-paradigma na JVM: funcional e orientada a objetos

Voltar às linguagens
Scala
72 cards encontrados
Categorias:
Versões:

Básico e Tipos


10 cards
Variáveis (val e var)
// val (imutável - preferir sempre):
val nome = "Ana"
val idade: Int = 30

// var (mutável - usar com moderação):
var contador = 0
contador += 1

// Type inference:
val x = 42       // Int
val pi = 3.14    // Double
val s = "olá"    // String
val b = true     // Boolean

// Lazy evaluation:
lazy val dados = carregarDados()

// Múltipla atribuição (destructuring):
val (a, b) = (1, 2)
val (x, y, z) = (10, 20, 30)

Use val por defeito (imutável) — o compilador impede reatribuição e permite otimizações. var só quando o estado precisa de mudar. lazy val adia a avaliação até ao primeiro acesso — ideal para recursos caros. A type inference do Scala é forte — raramente precisa anotar o tipo.

Tuples
// Criar:
val par = (1, "um")
val triplo = (1, "um", true)
val ponto = (3.0, 4.0)

// Acesso por posição:
par._1  // 1
par._2  // "um"

// Destructuring:
val (x, y) = (10, 20)
val (a, b, c) = (1, 2, 3)

// Retorno múltiplo de funções:
def dividir(a: Int, b: Int): (Int, Int) =
  (a / b, a % b)
val (q, r) = dividir(10, 3)

// zip (combinar coleções):
val pares = List(1,2,3).zip(List("a","b","c"))
// List((1,"a"), (2,"b"), (3,"c"))

Tuples agrupam valores heterogéneos sem criar uma classe — ideais para retornar múltiplos valores. Acesso por posição (_1, _2) ou destructuring na atribuição. zip combina duas coleções em pares. Para dados com significado semântico, prefira case class — tuples não documentam o que cada posição representa.

Any, Nothing e Unit
// Any (top type - super de tudo):
val qualquer: Any = 42
qualquer = "texto"  // OK
qualquer = List(1)  // OK

// AnyRef (= Java Object):
val ref: AnyRef = "string"

// AnyVal (primitivos):
val prim: AnyVal = 42

// Nothing (bottom type - subtipo de tudo):
def falhar(msg: String): Nothing =
  throw new Exception(msg)

// Usado para código unreachable:
def sair(): Nothing = sys.exit(1)

// Unit (sem valor significativo):
def log(msg: String): Unit =
  println(msg)

val u: Unit = ()  // único valor de Unit

Any é o top type (supertipo de tudo). Nothing é o bottom type (subtipo de tudo) — funções que retornam Nothing nunca completam normalmente (throw, sys.exit). Unit indica ausência de valor útil (como void) mas é um tipo real com valor (). Esta hierarquia permite polimorfismo total.

Tipos de dados
// Numéricos:
Byte, Short, Int, Long
Float, Double

// Outros:
Boolean, Char, String, Unit

// Literais:
val n = 42        // Int
val l = 42L       // Long
val f = 3.14f     // Float
val d = 3.14      // Double
val c = 'A'       // Char
val hex = 0xFF    // 255
val bin = 0b1010  // 10

// Hierarquia:
// Any -> AnyVal (primitivos)
// Any -> AnyRef (objetos = Java Object)

// Nothing (bottom type):
def erro(msg: String): Nothing =
  throw new Exception(msg)

Em Scala, tudo é objeto — Int, Boolean têm métodos (42.toString). Any é a raiz da hierarquia (AnyRef = objetos, AnyVal = primitivos). Nothing é o bottom type (subtipo de tudo) — usado para funções que nunca retornam normalmente. Unit equivale ao void mas é um tipo real com valor ().

Conversões de tipos
// toString:
42.toString      // "42"
s"$42"           // "42"

// toInt, toDouble, etc:
"42".toInt       // 42
"3.14".toDouble  // 3.14
3.14.toInt       // 3 (trunca)
42.toDouble      // 42.0
'A'.toInt        // 65

// Conversão segura (Option):
"42".toIntOption   // Some(42)
"abc".toIntOption  // None

// BigDecimal (precisão):
BigDecimal("3.14159")
BigDecimal(1) / BigDecimal(3)

// Formatação:
"%05d".format(42)   // "00042"
"%.2f".format(pi)   // "3.14"

Conversões em Scala são explícitas via métodos toX (toInt, toDouble). toInt lança exceção se inválido — use toIntOption para conversão segura que retorna Option. BigDecimal é essencial para valores monetários (Double tem erros de precisão). format usa sintaxe printf.

Type inference e anotações
// Inferência automática:
val x = 42          // Int
val s = "olá"       // String
val l = List(1,2,3) // List[Int]

// Quando anotar (necessário):
def soma(a: Int, b: Int): Int = a + b
val f: Int => Int = _ * 2

// Inferência em expressões:
val resultado = if true then 1 else 2  // Int
val msg = if false then "a" else 1     // Any

// Tipos em coleções:
val nums: List[Int] = List(1, 2, 3)
val map: Map[String, Int] = Map("a" -> 1)

// asInstanceOf (cast - evitar):
val n = qualquer.asInstanceOf[Int]

// isInstanceOf (verificar):
if qualquer.isInstanceOf[String] then
  println("é string")

A type inference do Scala é muito forte — raramente precisa anotar. Anotações são necessárias em parâmetros de funções, tipos de retorno públicos (boa prática) e quando o compilador não consegue inferir. asInstanceOf faz cast unchecked (evitar — preferir pattern matching). isInstanceOf verifica tipo em runtime. Prefira match com type patterns.

Strings e interpolação
// Interpolação s:
val nome = "Mundo"
s"Olá, $nome!"       // "Olá, Mundo!"
s"2+2 = ${2 + 2}"    // "2+2 = 4"

// Interpolação f (formatação):
f"$pi%.2f"           // "3.14"
f"$n%05d"            // "00042"

// Raw (sem escapes):
raw"linha1\nlinha2"  // literal \n

// Multi-line:
val texto = """
  |Primeira
  |Segunda
""".stripMargin

// Operações:
"olá".toUpperCase    // "OLÁ"
"a,b,c".split(",")   // Array("a","b","c")
"abc" * 3            // "abcabcabc"
" abc ".trim         // "abc"

Scala tem três interpoladores: s (substituição de variáveis), f (formatação printf-style como %.2f), e raw (sem processar escapes). Triple-quoted strings (\"\"\") preservam formatação multilinha — stripMargin remove o prefixo |. Todas as operações de String do Java estão disponíveis.

Option (null safety)
// Option encapsula valor opcional:
val nome: Option[String] = Some("Ana")
val vazio: Option[String] = None

// getOrElse (default seguro):
nome.getOrElse("Anónimo")   // "Ana"
vazio.getOrElse("Anónimo")  // "Anónimo"

// map/flatMap (propaga None):
nome.map(_.toUpperCase)     // Some("ANA")
vazio.map(_.toUpperCase)    // None

// Pattern matching:
nome match
  case Some(n) => println(n)
  case None    => println("Vazio")

// for comprehension:
for n <- nome do println(n)

// Convenção: nunca usar null
// APIs retornam Option

Option é a alternativa de Scala ao null — encapsula um valor que pode ou não existir (Some/None). map/flatMap propagam None automaticamente (sem NullPointerException). getOrElse fornece default seguro. A convenção em Scala é nunca usar null — APIs bem desenhadas retornam Option.

Operadores
// Aritméticos:
+ - * / %

// Comparação:
== != > < >= <=
// == compara VALORES (não referências!)

// Lógicos:
&& || !

// Bitwise:
& | ^ ~ << >>

// Range:
1 to 10      // inclusivo (1..10)
1 until 10   // exclusivo (1..9)
1 to 10 by 2 // step (1,3,5,7,9)

// Operadores são métodos:
1 + 2 == 1.+(2)
"abc".reverse == "cba"

// Atribuição composta:
+= -= *= /= %=

Em Scala, operadores são métodos — 1 + 2 é açúcar para 1.+(2). Pode definir operadores customizados nas suas classes. == compara valores (usa equals), não referências (diferente de Java). Ranges (to = inclusivo, until = exclusivo, by = step) são usados extensivamente em for loops.

Ranges e sequências
// Range inclusivo:
val r1 = 1 to 10      // Range(1,2,...,10)

// Range exclusivo:
val r2 = 1 until 10   // Range(1,2,...,9)

// Com step:
val pares = 2 to 20 by 2    // 2,4,6,...,20
val desc = 10 to 1 by -1    // 10,9,...,1

// Converter para List:
val lista = (1 to 5).toList  // List(1,2,3,4,5)

// Operações em ranges:
(1 to 10).sum       // 55
(1 to 10).map(_ * 2)
(1 to 10).filter(_ % 2 == 0)
(1 to 10).foreach(println)

// Range de chars:
('a' to 'z').toList  // List(a,b,...,z)

// Verificar pertença:
(1 to 10).contains(5)  // true

Ranges são sequências lazy de inteiros — to é inclusivo, until exclusivo, by define o step. Funcionam com chars ('a' to 'z'). Suportam todas as operações de coleções (map, filter, sum). São a forma idiomática de iterar em Scala — substituem loops indexados de linguagens imperativas.

Funções


9 cards
Definir funções
// def (método):
def somar(a: Int, b: Int): Int = a + b

// Multi-linha (Scala 3 indentação):
def fatorial(n: Int): Int =
  if n <= 1 then 1
  else n * fatorial(n - 1)

// Default e named args:
def conectar(host: String, porta: Int = 3306): Unit =
  println(s"$host:$porta")
conectar("localhost")
conectar("db", porta = 5432)

// Varargs:
def soma(nums: Int*): Int = nums.sum
soma(1, 2, 3)  // 6

// Single expression (tipo inferido):
def dobro(n: Int) = n * 2

def define métodos (avaliados a cada chamada). O tipo de retorno pode ser inferido em expressões simples. Default args eliminam sobrecargas. Varargs (Int*) aceita N argumentos. Named args (porta = 5432) melhoram legibilidade. Em Scala 3, a indentação define blocos (sem chavetas).

Recursão e @tailrec
// Recursão simples:
def fatorial(n: Int): BigInt =
  if n <= 1 then 1
  else n * fatorial(n - 1)

// Tail recursion (otimizada):
import scala.annotation.tailrec

@tailrec
def fatorialTR(n: Int, acc: BigInt = 1): BigInt =
  if n <= 1 then acc
  else fatorialTR(n - 1, n * acc)

// Fibonacci com tail recursion:
@tailrec
def fib(n: Int, a: BigInt = 0, b: BigInt = 1): BigInt =
  if n == 0 then a
  else fib(n - 1, b, a + b)

// LazyList (sequências infinitas):
lazy val fibs: LazyList[BigInt] =
  BigInt(0) #:: BigInt(1) #::
  fibs.zip(fibs.tail).map(_ + _)

fibs.take(10).toList

Recursão substitui loops imperativos em FP. @tailrec garante que o compilador otimiza para loop (sem stack overflow) — se não for tail-recursive, dá erro de compilação. O acumulador (acc) carrega o resultado parcial. LazyList permite sequências infinitas — só calcula os elementos necessários (take(10) não calcula o resto).

Funções inline e transparent
// inline (elimina overhead em compilação):
inline def max(inline a: Int, inline b: Int): Int =
  if a > b then a else b

// Sem overhead de chamada:
val x = max(10, 20)  // compilado como if direto

// transparent inline (tipo preciso):
transparent inline def get(id: Int): Any =
  inline if id == 1 then "string"
  else 42

val s: String = get(1)  // OK! tipo = String
val n: Int = get(2)     // OK! tipo = Int

// inline given:
inline given intOrd: Ordering[Int] =
  Ordering.Int

// compiletime operations:
import scala.compiletime.*
inline def tamanho[T]: Int =
  constValue[Tuple.Size[T]]

inline (Scala 3) elimina overhead de abstração — o código é expandido no call site em compilação. transparent inline permite que o compilador infira o tipo preciso (não o tipo declarado). inline if é resolvido em compilação. Essencial para bibliotecas de alto desempenho e type-level programming. Zero custo runtime para abstrações.

Lambdas e funções anónimas
// Sintaxe completa:
val soma = (a: Int, b: Int) => a + b

// Type inference:
val dobro = (x: Int) => x * 2
val dobro2: Int => Int = _ * 2

// Placeholder syntax (_):
List(1,2,3).map(_ * 2)
List(1,2,3).filter(_ > 1)
List(1,2,3).reduce(_ + _)

// Sem parâmetros:
val agora = () => System.currentTimeMillis()

// Como argumento:
def aplicar(f: Int => Int, x: Int): Int = f(x)
aplicar(_ * 3, 5)  // 15

// Múltiplos underscores:
// _ + _ == (a, b) => a + b
List(1,2,3).reduce(_ + _)  // 6

Lambdas são valores de primeira classe em Scala. O underscore (_) é placeholder para o parâmetro — _ * 2 equivale a x => x * 2. Cada _ representa um parâmetro diferente (_ + _ = (a,b) => a + b). A sintaxe placeholder torna operações em coleções extremamente concisas.

Funções como valores
// Atribuir a val:
val dobro: Int => Int = _ * 2
dobro(5)  // 10

// Lista de funções:
val ops: List[Int => Int] = List(_ + 1, _ * 2, _ - 3)
ops.map(f => f(10))  // List(11, 20, 7)

// Retornar função (closure):
def multiplicador(fator: Int): Int => Int =
  _ * fator
val triplo = multiplicador(3)
triplo(5)  // 15

// Closure com estado:
def contador() =
  var n = 0
  () => { n += 1; n }

// Function traits:
// Function0[R], Function1[T,R], Function2[T1,T2,R]
val f: Function1[Int, Int] = _ * 2
val g: Int => Int = _ * 2  // equivalente

Funções são valores de primeira classe — podem ser armazenadas, passadas, retornadas e colocadas em coleções. Int => Int é açúcar para Function1[Int, Int]. Closures capturam variáveis do ambiente (contador mantém estado). Fábricas de funções (multiplicador) criam comportamento parametrizado sem classes.

Higher-order functions
val nums = List(1, 2, 3, 4, 5)

// map (transformar):
nums.map(_ * 2)  // List(2,4,6,8,10)

// filter (selecionar):
nums.filter(_ % 2 == 0)  // List(2,4)

// reduce / fold (agregar):
nums.reduce(_ + _)       // 15
nums.fold(0)(_ + _)      // 15
nums.foldLeft(0)(_ + _)  // 15

// flatMap (mapear e achatar):
List("ab","cd").flatMap(_.toList)
// List(a, b, c, d)

// Composição de funções:
val f = (x: Int) => x * 2
val g = (x: Int) => x + 1
val h = f compose g   // f(g(x))
val k = f andThen g   // g(f(x))

// foreach (side effect):
nums.foreach(println)

HOFs recebem ou retornam funções — a base da programação funcional. map transforma, filter seleciona, fold/reduce agregam. flatMap mapeia e achata. compose/andThen compõem funções (f∘g vs g∘f). Estas operações encadeiam-se em pipelines sem variáveis mutáveis.

Métodos vs Funções
// Método (def):
def metodo(x: Int): Int = x * 2

// Função (val):
val funcao: Int => Int = x => x * 2

// Eta expansion (método -> função):
val f = metodo _   // Int => Int
val g = metodo     // Scala 3: auto-converte

// Diferenças:
// - def é avaliado a cada chamada
// - val é avaliado uma vez
// - def pode ter type params
// - val é objeto (Function1)

// By-name parameters:
def repetir(n: Int)(bloco: => Unit): Unit =
  (1 to n).foreach(_ => bloco)

repetir(3) {
  println("Olá")  // executado 3x
}

def é um método (avaliado a cada chamada, pode ter type parameters, não é um valor). val com tipo função é um objeto Function1 (avaliado uma vez, pode ser passado). By-name parameters (=> Unit) adiam a avaliação do argumento — o bloco é reavaliado a cada uso (essencial para DSLs e controlo de fluxo customizado).

Currying e partial application
// Currying (múltiplas listas de params):
def soma(a: Int)(b: Int): Int = a + b
soma(3)(4)  // 7

// Partial application:
val soma3 = soma(3) _
soma3(4)   // 7
soma3(10)  // 13

// Múltiplos parâmetros:
def formatar(prefix: String)(valor: Int)(sufixo: String) =
  s"$prefix$valor$sufixo"

val euros = formatar("€")(_: Int)("")
euros(42)  // "€42"

// fold com currying:
val nums = List(1, 2, 3, 4, 5)
nums.foldLeft(0)(_ + _)   // 15
nums.foldLeft("")(_ + _)  // "12345"

// Pipe operator (Scala 3):
val resultado = 5
  |> (x => x * 2)
  |> (x => x + 1)  // 11

Currying transforma uma função de N parâmetros em N funções de 1 parâmetro — permite partial application (fixar argumentos). Múltiplas listas de parâmetros são idiomáticas (foldLeft(0)(_ + _)). O pipe operator |> (Scala 3) encadeia transformações da esquerda para direita — mais legível que composição aninhada.

PartialFunction
// PartialFunction (definida para subset):
val pf: PartialFunction[Int, String] =
  case 1 => "um"
  case 2 => "dois"

pf(1)          // "um"
pf.isDefinedAt(3)  // false
pf(3)          // MatchError!

// collect usa PartialFunction:
List(1, 2, 3, 4).collect {
  case n if n % 2 == 0 => n * 10
}
// List(20, 40)

// orElse (compor parciais):
val par: PartialFunction[Int, String] =
  case n if n % 2 == 0 => "par"
val impar: PartialFunction[Int, String] =
  case n if n % 2 == 1 => "ímpar"
val todos = par orElse impar

// andThen (transformar resultado):
val duplo = pf andThen (_ + "!")

PartialFunction só está definida para certos inputs — usada em pattern matching e collect. isDefinedAt verifica se o input é válido. orElse compõe parciais (fallback). andThen transforma o resultado. Essencial em atores (Akka) e routing. Diferente de Function1: pode não estar definida para todos os inputs do tipo.

Classes e Traits


9 cards
Classes
class Pessoa(val nome: String, var idade: Int):
  def saudacao: String = s"Olá, sou $nome"
  def envelhecer(): Unit = idade += 1

// Uso:
val p = Pessoa("Ana", 30)
p.nome       // "Ana" (val = getter)
p.idade = 31 // OK (var = setter)
p.saudacao

// Construtor auxiliar:
class Ponto(val x: Double, val y: Double):
  def this() = this(0, 0)
  def distancia: Double = math.sqrt(x*x + y*y)

// Private constructor:
class Config private (val host: String):
  def this() = this("localhost")

// Override:
override def toString = s"Ponto($x, $y)"

Parâmetros com val/var no construtor tornam-se campos automaticamente (sem boilerplate). val = getter apenas (imutável), var = getter + setter. Construtores auxiliares (def this) devem chamar o primário. private constructor força uso de factory methods. Scala 3 usa indentação em vez de chavetas.

Generics
// Classe genérica:
class Caixa[T](val conteudo: T):
  def map[U](f: T => U): Caixa[U] =
    Caixa(f(conteudo))

val c = Caixa(42)
val s = c.map(_.toString)  // Caixa[String]

// Bounds:
def maximo[T <: Comparable[T]](a: T, b: T): T =
  if a.compareTo(b) > 0 then a else b

// Context bounds (type class):
def ordenar[T: Ordering](lista: List[T]): List[T] =
  lista.sorted

// Variance:
class Covariante[+T]    // Producer
class Contravariante[-T] // Consumer
class Invariante[T]

// Wildcards:
def tamanho(l: List[_]): Int = l.size

Generics em Scala são mais expressivos que Java: +T (covariante — Caixa[Dog] é subtipo de Caixa[Animal]), -T (contravariante), T (invariante). Upper bound (<:) exige subtipo, context bound (T: Ordering) exige instância given. Variance annotations são verificadas pelo compilador.

Opaque types e value classes
// Opaque type (type-safe, zero overhead):
opaque type UserId = Int
object UserId:
  def apply(id: Int): UserId = id
  extension (id: UserId)
    def value: Int = id

opaque type Email = String
object Email:
  def from(s: String): Option[Email] =
    if s.contains("@") then Some(s) else None

// Uso (type-safe):
def buscar(id: UserId): User = ???
val uid = UserId(42)
buscar(uid)    // OK
// buscar(42)  // ERRO! Int != UserId

// Value class (Scala 2, ainda válido):
case class Metros(val value: Double) extends AnyVal:
  def +(o: Metros) = Metros(value + o.value)

val d = Metros(100) + Metros(50)

Opaque types (Scala 3) criam tipos distintos sem overhead runtime — UserId é Int em runtime mas tipo separado em compilação. Previnem erros de misturar valores (UserId vs OrderId). Value classes (extends AnyVal) são a alternativa Scala 2. Ambos eliminam a classe de bugs "primitive obsession" com custo zero.

Case classes
case class User(nome: String, idade: Int, email: String)

// Automático: equals, hashCode, toString, copy
val u1 = User("Ana", 30, "a@b.c")
val u2 = u1.copy(idade = 31)

// Pattern matching:
u1 match
  case User(nome, idade, _) => s"$nome: $idade"

// Destructuring:
val User(n, i, _) = u1

// Sealed hierarchy (ADT):
sealed trait Forma
case class Circulo(raio: Double) extends Forma
case class Retangulo(l: Double, a: Double) extends Forma
case object Ponto extends Forma

def area(f: Forma): Double = f match
  case Circulo(r) => math.Pi * r * r
  case Retangulo(l, a) => l * a
  case Ponto => 0

case class gera automaticamente equals, hashCode, toString, copy e unapply (para pattern matching). copy cria cópia com campos alterados — essencial para atualização imutável. sealed trait + case classes formam ADTs — o compilador verifica exaustividade no match.

Type classes
// Definir type class:
trait JsonEncoder[T]:
  def encode(value: T): String

// Instâncias (given):
given JsonEncoder[Int] with
  def encode(value: Int) = value.toString

given JsonEncoder[String] with
  def encode(value: String) = s"\"$value\""

// Instância derivada (recursiva):
given [T](using enc: JsonEncoder[T]): JsonEncoder[List[T]] with
  def encode(value: List[T]) =
    value.map(enc.encode).mkString("[", ",", "]")

// Usar com context bound:
def toJson[T: JsonEncoder](value: T): String =
  summon[JsonEncoder[T]].encode(value)

toJson(42)          // "42"
toJson(List(1,2,3)) // "[1,2,3]"

Type classes permitem polimorfismo ad-hoc sem herança — define comportamento (trait), cria instâncias para tipos específicos (given), e usa com context bounds. A instância para List[T] é derivada automaticamente se existir instância para T. summon[T] obtém a instância given em scope. Padrão central em Cats e Circe.

Traits e mixins
// Trait (interface + implementação):
trait Nadador:
  def nadar(): String = "A nadar"

trait Voador:
  def voar(): String

// Implementar múltiplas:
class Pato extends Nadador, Voador:
  def voar(): String = "A voar (baixo)"

// Trait com estado:
trait Logger:
  var logs: List[String] = Nil
  def log(msg: String): Unit =
    logs = msg :: logs

// Stackable traits:
trait Base:
  def processar(s: String): String = s

trait Upper extends Base:
  override def processar(s: String) =
    super.processar(s).toUpperCase

trait Exclaim extends Base:
  override def processar(s: String) =
    super.processar(s) + "!"

Traits são mais poderosas que interfaces — podem ter implementação, estado e ser compostas via mixins. Stackable traits permitem compor comportamentos em cadeia (Upper + Exclaim). A ordem de mixin importa — super chama a trait anterior na cadeia (linearização). Uma classe pode estender múltiplas traits (sem diamond problem).

Objects e companions
// Singleton object:
object AppConfig:
  val versao = "1.0"
  def iniciar(): Unit = println("Iniciado")

AppConfig.iniciar()

// Companion object:
class User private (val nome: String, val idade: Int)

object User:
  // Factory method:
  def apply(nome: String, idade: Int): User =
    new User(nome, idade)

  // Extractor:
  def unapply(u: User): Option[(String, Int)] =
    Some((u.nome, u.idade))

  val anonimo = new User("Anónimo", 0)

// apply permite:
val u = User("Ana", 30)  // sem new

// main method (Scala 3):
@main def app(): Unit =
  println("Olá Scala 3")

object define singletons (uma instância por JVM). Companion object partilha nome com uma classe e tem acesso mútuo a membros privados. apply() é o factory method convencional — permite User("Ana") sem new. unapply() habilita pattern matching e destructuring. @main (Scala 3) define pontos de entrada sem object com def main.

Herança e abstract classes
// Abstract class:
abstract class Animal(val nome: String):
  def som(): String
  def descricao: String = s"$nome faz ${som()}"

class Cao(nome: String) extends Animal(nome):
  def som(): String = "Au!"

class Gato(nome: String) extends Animal(nome):
  def som(): String = "Miau!"

// Final e sealed:
final class Imutavel   // não pode ser estendida
sealed trait Expr      // filhos no mesmo ficheiro

// Object (singleton):
object Database:
  private var conn: Connection = null
  def conectar(): Unit = ???

// Companion object:
class User(val nome: String)
object User:
  def apply(nome: String) = new User(nome)
  def anonimo = new User("Anónimo")

Scala suporta herança simples (extends) com abstract class para código partilhado. sealed restringe subclasses ao mesmo ficheiro — essencial para pattern matching exaustivo. object define singletons. Companion object (mesmo nome da classe) define factory methods (apply permite User("Ana") sem new). ??? lança NotImplementedError.

Enum (Scala 3)
// Enum simples:
enum Cor:
  case Vermelho, Verde, Azul

// Enum com parâmetros:
enum Planeta(val massa: Double, val raio: Double):
  case Mercurio extends Planeta(3.303e+23, 2.4397e6)
  case Terra extends Planeta(5.976e+24, 6.37814e6)

  def gravidade: Double =
    6.67300E-11 * massa / (raio * raio)

// Enum ADT (com case classes):
enum Forma:
  case Circulo(r: Double)
  case Retangulo(l: Double, a: Double)

// Pattern matching:
def descrever(c: Cor): String = c match
  case Cor.Vermelho => "quente"
  case Cor.Verde => "neutro"
  case Cor.Azul => "frio"

// values e valueOf:
Cor.values       // Array(Vermelho, Verde, Azul)
Cor.valueOf("Verde")

enum (Scala 3) é açúcar para sealed trait + case object/case class. Suporta parâmetros, métodos e pattern matching exaustivo. values lista todos os casos, valueOf converte string. Enums com parâmetros (como Planeta) substituem padrões verbosos do Scala 2. Ideal para conjuntos finitos de variantes.

Coleções


10 cards
List e Seq
// Criar:
val lista = List(1, 2, 3, 4, 5)
val vazia = List.empty[Int]
val range = (1 to 10).toList
val fill = List.fill(5)(0)

// Acesso:
lista.head   // 1
lista.tail   // List(2,3,4,5)
lista.last   // 5
lista.init   // List(1,2,3,4)
lista.length // 5

// Construção:
0 :: lista       // prepend O(1)
lista :+ 6       // append O(n)
lista ++ List(6, 7)  // concat

// Pattern matching:
lista match
  case Nil => "vazia"
  case h :: t => s"head=$h"

// Vector (acesso O(1)):
val v = Vector(1, 2, 3)

List é uma linked list imutável — prepend (::) é O(1), acesso por índice é O(n). Ideal para recursão e quando adiciona sempre à cabeça. Vector oferece acesso O(1) a qualquer índice (árvore trie). head/tail decompõem para pattern matching. Todas as coleções são imutáveis por defeito.

Ordenação e transformação
val pessoas = List(
  ("Ana", 30), ("Rui", 25), ("Eva", 35)
)

// Ordenar:
pessoas.sortBy(_._2)   // por idade (asc)
pessoas.sortBy(_._1)   // por nome
pessoas.sortWith((a, b) => a._2 > b._2) // desc

// distinct:
List(1,2,2,3,3,3).distinct  // List(1,2,3)

// sliding / grouped:
List(1,2,3,4,5).sliding(3).toList
// List(List(1,2,3), List(2,3,4), List(3,4,5))
List(1,2,3,4,5).grouped(2).toList
// List(List(1,2), List(3,4), List(5))

// flatten / transpose:
List(List(1,2), List(3,4)).flatten
List(List(1,2), List(3,4)).transpose

// scanLeft (fold com intermédios):
List(1,2,3).scanLeft(0)(_ + _)  // List(0,1,3,6)

sortBy ordena por chave extraída (ascendente); sortWith usa comparador customizado. sliding cria janelas sobrepostas (médias móveis), grouped divide em blocos (paginação). flatten achata coleções aninhadas, transpose troca linhas por colunas. scanLeft retorna todos os resultados intermédios do fold.

groupBy e transformações
val palavras = List("gato", "cão", "galinha", "coelho")

// groupBy:
palavras.groupBy(_.head)
// Map(g -> List(gato, galinha), c -> List(cão, coelho))

// Contar frequências:
val texto = "banana"
texto.groupBy(identity).map((c, cs) => (c, cs.length))
// Map(b -> 1, a -> 3, n -> 2)

// mapValues (transformar valores):
val scores = Map("Ana" -> 95, "Rui" -> 87)
scores.map((k, v) => (k, v + 5))

// flatMap com Option:
val nums = List("1", "abc", "3")
nums.flatMap(_.toIntOption)  // List(1, 3)

// unzip:
val pares = List((1,"a"), (2,"b"), (3,"c"))
val (ints, strs) = pares.unzip

// mkString:
List(1,2,3).mkString(", ")  // "1, 2, 3"

groupBy agrupa por chave retornando Map[K, List[V]]. flatMap com toIntOption filtra e converte numa operação (ignora None). unzip separa lista de tuplos em duas listas. mkString junta elementos com separador. Estas operações combinam-se para transformações complexas de dados de forma declarativa.

Map e Set
// Map:
val scores = Map("Ana" -> 95, "Rui" -> 87)

// Acesso:
scores("Ana")              // 95 (throw se não existe)
scores.getOrElse("Eva", 0) // 0
scores.get("Ana")          // Some(95)
scores.contains("Ana")     // true

// Modificar (imutável = novo Map):
val novo = scores + ("Eva" -> 100)
val sem = scores - "Rui"
val merged = scores ++ Map("Novo" -> 50)

// Iterar:
for (nome, score) <- scores do
  println(s"$nome: $score")

// Set:
val cores = Set("red", "green", "blue")
cores + "yellow"
cores.contains("red")        // true
Set(1,2,3) & Set(2,3,4)     // interseção
Set(1,2,3) | Set(3,4,5)     // união

Map imutável retorna novo Map em cada operação (+, -, ++) — o original nunca muda. get() retorna Option (seguro), apply() lança exceção se chave não existir. Set garante unicidade e suporta operações matemáticas (& interseção, | união). Destructuring (nome, score) <- scores é idiomático.

LazyList e views
// LazyList (avaliação preguiçosa):
lazy val naturais: LazyList[Int] =
  1 #:: naturais.map(_ + 1)

naturais.take(10).toList  // List(1..10)

// Fibonacci infinito:
lazy val fibs: LazyList[BigInt] =
  BigInt(0) #:: BigInt(1) #::
  fibs.zip(fibs.tail).map(_ + _)

fibs.take(10).toList

// View (sem coleções intermédias):
val resultado = (1 to 1000000).view
  .filter(_ % 2 == 0)
  .map(_ * _)
  .take(5)
  .toList

// Iterator (single-use):
val iter = Iterator.from(1)
iter.take(5).toList  // List(1,2,3,4,5)

LazyList avalia elementos sob demanda — permite sequências infinitas sem memória infinita. view evita coleções intermédias em cadeias de operações (processa elemento a elemento). Iterator é single-use (não pode re-iterar). Use view para performance em coleções grandes com múltiplas transformações. #:: é o construtor lazy (como :: para List).

zip, sliding e combinadores
// zip (combinar em pares):
val a = List(1, 2, 3)
val b = List("a", "b", "c")
a.zip(b)  // List((1,"a"), (2,"b"), (3,"c"))

// zipWithIndex:
a.zipWithIndex  // List((1,0), (2,1), (3,2))

// zipAll (com default):
List(1,2).zipAll(List("a","b","c"), 0, "?")

// sliding (janelas):
List(1,2,3,4,5).sliding(3).toList
// List(List(1,2,3), List(2,3,4), List(3,4,5))

// grouped (blocos):
List(1,2,3,4,5).grouped(2).toList
// List(List(1,2), List(3,4), List(5))

// combinations / permutations:
List(1,2,3).combinations(2).toList
List(1,2,3).permutations.toList

// tabulate:
List.tabulate(5)(n => n * n)  // List(0,1,4,9,16)

zip combina duas coleções em pares (para no mais curto). zipWithIndex adiciona índices. sliding cria janelas sobrepostas (médias móveis), grouped divide em blocos fixos. combinations/permutations geram combinações matemáticas. tabulate cria lista a partir de função do índice. Todos retornam iterators lazy.

Operações funcionais
val nums = List(1, 2, 3, 4, 5, 6, 7, 8)

// Transformação:
nums.map(_ * 2)
nums.filter(_ % 2 == 0)
nums.flatMap(n => List(n, n * 10))
nums.collect { case n if n > 5 => n * 2 }

// Agregação:
nums.reduce(_ + _)    // 36
nums.fold(0)(_ + _)   // 36
nums.sum              // 36
nums.count(_ % 2 == 0) // 4

// Pesquisa:
nums.find(_ > 5)       // Some(6)
nums.exists(_ > 7)     // true
nums.forall(_ > 0)     // true

// Agrupamento:
nums.groupBy(_ % 2)
nums.partition(_ % 2 == 0)
nums.zip(nums.map(_ * 2))

collect combina filter + map com pattern matching. foldLeft acumula da esquerda (ordem importa). find retorna Option. groupBy agrupa por chave (retorna Map). partition divide em dois grupos (tuplo de coleções). Estas operações encadeiam-se em pipelines funcionais sem variáveis mutáveis.

Array e coleções mutáveis
// Array (mutável, tamanho fixo):
val arr = Array(1, 2, 3, 4, 5)
arr(0) = 10  // mutável!
arr.length   // 5

// ArrayBuffer (tamanho variável):
import scala.collection.mutable.ArrayBuffer
val buf = ArrayBuffer(1, 2, 3)
buf += 4
buf ++= List(5, 6)
buf.remove(0)

// HashMap mutável:
import scala.collection.mutable
val map = mutable.Map("a" -> 1)
map("b") = 2
map += ("c" -> 3)
map -= "a"

// Converter:
val imutavel = buf.toList
val mutavel = lista.to(ArrayBuffer)

// Convenção: imutável por defeito

Array é o array Java com tamanho fixo e mutação in-place. ArrayBuffer cresce dinamicamente (como ArrayList). Coleções mutáveis (mutable.Map, mutable.Set) modificam in-place — mais performáticas mas menos seguras em código concorrente. A convenção é usar imutáveis por defeito e mutáveis apenas em hot paths medidos.

Option, Either, Try
// Option (valor ausente):
val opt: Option[Int] = Some(42)
opt.map(_ * 2)         // Some(84)
opt.getOrElse(0)       // 42
opt.filter(_ > 40)     // Some(42)

// Either (erro explícito):
type Result[A] = Either[String, A]
def parse(s: String): Result[Int] =
  s.toIntOption match
    case Some(n) => Right(n)
    case None => Left(s"Inválido: $s")

// For comprehension:
val total = for
  a <- parse("10")
  b <- parse("20")
yield a + b  // Right(30)

// Try (exceção capturada):
import scala.util.Try
Try("42".toInt)   // Success(42)
Try("abc".toInt)  // Failure(...)

Três tipos para representar falha: Option (valor ausente, sem info de erro), Either (erro explícito — Left=erro, Right=sucesso), Try (exceção capturada). Either é o preferido para erros de domínio porque documenta o tipo de erro. Todos funcionam em for comprehensions — None/Left/Failure faz short-circuit.

fold, reduce e scan
val nums = List(1, 2, 3, 4, 5)

// reduce (sem seed):
nums.reduce(_ + _)       // 15
nums.reduce(_ * _)       // 120
nums.reduceLeft(_ - _)   // (((1-2)-3)-4)-5 = -13

// fold (com seed):
nums.fold(0)(_ + _)      // 15
nums.fold("")(_ + _)     // "12345"
nums.fold(1)(_ * _)      // 120

// foldLeft vs foldRight:
nums.foldLeft(0)(_ - _)   // -15
nums.foldRight(0)(_ - _)  // 3

// scan (resultados intermédios):
nums.scanLeft(0)(_ + _)   // List(0,1,3,6,10,15)

// Aplicações:
val max = nums.reduce(_ max _)
val concat = nums.map(_.toString).reduce(_ + _)
val freq = nums.foldLeft(Map.empty[Int, Int]) { (m, n) =>
  m.updated(n, m.getOrElse(n, 0) + 1)
}

reduce agrega sem valor inicial (falha em lista vazia). fold aceita seed (seguro em vazias). foldLeft processa da esquerda (eficiente em List), foldRight da direita (ordem importa para operações não-comutativas). scanLeft retorna todos os passos intermédios. fold é a operação mais geral — map/filter podem ser expressos com fold.

Avançado


9 cards
Futures e concorrência
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

// Criar Future:
val futuro = Future {
  Thread.sleep(1000)
  42
}

// Transformar:
val resultado = futuro
  .map(_ * 2)
  .filter(_ > 50)
  .recover { case _ => 0 }

// For comprehension:
val total = for
  a <- Future(10)
  b <- Future(20)
yield a + b  // Future(30)

// Sequência de Futures:
val futures = List(Future(1), Future(2), Future(3))
val todos = Future.sequence(futures)

// Await (bloquear - evitar):
import scala.concurrent.Await
import scala.concurrent.duration._
val valor = Await.result(futuro, 5.seconds)

Future representa um valor calculado assincronamente. map/filter/recover transformam sem bloquear. For comprehension com Futures executa em sequência. Future.sequence converte List[Future[A]] em Future[List[A]]. Await.result bloqueia a thread — evitar em produção. recover trata falhas (como catch).

SBT e projeto
// build.sbt:
name := "meu-projeto"
version := "1.0"
scalaVersion := "3.3.1"

libraryDependencies ++= Seq(
  "org.typelevel" %% "cats-core" % "2.10.0",
  "org.scalatest" %% "scalatest" % "3.2.17" % Test
)

// Comandos SBT:
// sbt compile
// sbt run
// sbt test
// sbt console (REPL)
// sbt assembly (fat jar)

// Estrutura:
// src/main/scala/
// src/test/scala/
// project/build.properties

// scala-cli (scripts rápidos):
// scala-cli run app.scala

// Ammonite (REPL avançado):
// amm

SBT (Scala Build Tool) é o build standard — gere compilação, dependências, testes e publicação. %% adiciona a versão do Scala automaticamente ao artifact. % Test marca dependências só para testes. scala-cli é a ferramenta moderna para scripts rápidos (single-file). sbt assembly gera fat JAR para deployment.

Testing com ScalaTest
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers

class StackSpec extends AnyFlatSpec with Matchers:

  "A Stack" should "pop values in LIFO order" in {
    val stack = scala.collection.mutable.Stack[Int]()
    stack.push(1)
    stack.push(2)
    stack.pop() should be (2)
    stack.pop() should be (1)
  }

  it should "throw when empty" in {
    val stack = scala.collection.mutable.Stack[Int]()
    an [NoSuchElementException] should be thrownBy {
      stack.pop()
    }
  }

// Property-based testing:
import org.scalatestplus.scalacheck._
"Sorting" should "preserve length" in {
  forAll { (xs: List[Int]) =>
    xs.sorted.length shouldEqual xs.length
  }
}

ScalaTest é o framework de testes standard — AnyFlatSpec com Matchers fornece DSL legível (should be, shouldEqual). Suporta múltiplos estilos (FlatSpec, FunSuite, WordSpec). ScalaCheck adiciona property-based testing (gera inputs aleatórios e verifica invariantes). Testes são executados com sbt test.

Implicits e Given/Using
// Scala 3 - Given/Using:
trait Show[T]:
  def show(value: T): String

given intShow: Show[Int] with
  def show(v: Int) = s"Int($v)"

given listShow[T](using s: Show[T]): Show[List[T]] with
  def show(v: List[T]) =
    v.map(s.show).mkString("[", ",", "]")

def render[T](value: T)(using s: Show[T]): String =
  s.show(value)

render(42)           // "Int(42)"
render(List(1,2,3))  // "[Int(1),Int(2),Int(3)]"

// summon (obter instância):
val s = summon[Show[Int]]

// Extension methods com given:
extension [T](value: T)(using s: Show[T])
  def showStr: String = s.show(value)

given/using é o mecanismo de resolução implícita do Scala 3 — o compilador procura automaticamente instâncias given em scope. Permite type classes sem passing manual de dependências. summon[T] obtém a instância explicitamente. Extension methods com given adicionam métodos condicionais. Substitui os implicit confusos do Scala 2.

Cats e FP libraries
import cats._
import cats.implicits._

// Applicative (combinar N effects):
(Option(1), Option(2)).mapN(_ + _)  // Some(3)
(Option(1), None).mapN(_ + _)       // None

// Traverse:
List("1","2","3").traverse(_.toIntOption)
// Some(List(1,2,3))

// Validated (acumular erros):
import cats.data.ValidatedNel
type Errors[A] = ValidatedNel[String, A]

def validarIdade(n: Int): Errors[Int] =
  if n >= 0 then n.validNel
  else "Idade inválida".invalidNel

// IO (Cats Effect):
import cats.effect.IO
val programa: IO[Unit] = for
  _ <- IO.println("Nome?")
  nome <- IO.readLine
  _ <- IO.println(s"Olá, $nome!")
yield ()

Cats fornece type classes (Functor, Monad, Applicative) e operações universais. mapN combina N Options/Eithers sem for comprehension. traverse converte List[Option[A]] em Option[List[A]]. Validated acumula todos os erros (Either faz short-circuit no primeiro). Cats Effect IO gere efeitos colaterais de forma pura e testável.

Monads e for comprehension
// Option como monad:
def buscarUser(id: Int): Option[User] = ???
def buscarEmail(u: User): Option[String] = ???

val email = for
  user <- buscarUser(1)
  email <- buscarEmail(user)
yield email  // Option[String]

// Either como monad:
type Result[A] = Either[String, A]
def validar(s: String): Result[Int] =
  s.toIntOption.toRight(s"Inválido: $s")

val total: Result[Int] = for
  a <- validar("10")
  b <- validar("20")
yield a + b

// Future como monad:
val resultado: Future[String] = for
  user <- fetchUser(1)
  posts <- fetchPosts(user.id)
yield s"${user.nome}: ${posts.size} posts"

Um Monad é qualquer tipo com map e flatMapOption, Either, Future, List, Try. For comprehension é açúcar para flatMap/map encadeados (cada <- é um flatMap, yield é map). Se algum passo retorna None/Left/Failure, os seguintes são ignorados (short-circuit).

Extension methods
// Extension simples:
extension (s: String)
  def gritar: String = s.toUpperCase + "!"
  def palavras: List[String] = s.split(" ").toList
  def truncar(n: Int): String =
    if s.length <= n then s else s.take(n) + "..."

"olá mundo".gritar      // "OLÁ MUNDO!"
"a b c".palavras         // List("a","b","c")
"texto longo".truncar(5) // "texto..."

// Extension genérica:
extension [T](lista: List[T])
  def segundo: Option[T] = lista.lift(1)
  def pares: List[(T, T)] =
    lista.sliding(2).collect { case List(a, b) => (a, b) }.toList

List(1,2,3).segundo  // Some(2)

// Extension com given:
extension [T](value: T)(using s: Show[T])
  def showStr: String = s.show(value)

Extension methods (Scala 3) adicionam métodos a tipos existentes sem wrapper classes nem inheritance. Podem ser genéricos ([T]) e condicionais (using). Substituem implicit classes do Scala 2 com sintaxe mais clara. Ideais para utilitários em String, List e tipos de domínio. O método é resolvido estaticamente — zero overhead runtime.

Sistema de tipos avançado
// Union types (Scala 3):
def processar(x: Int | String): String = x match
  case n: Int => s"Número: $n"
  case s: String => s"Texto: $s"

// Intersection types:
trait A { def a(): Unit }
trait B { def b(): Unit }
def usar(x: A & B): Unit = { x.a(); x.b() }

// Opaque types:
opaque type UserId = Int
object UserId:
  def apply(id: Int): UserId = id

// Match types:
type Elem[X] = X match
  case String => Char
  case List[t] => t
  case Array[t] => t

type A = Elem[String]     // Char
type B = Elem[List[Int]]  // Int

Scala 3 introduz union types (A | B — substitui overloads), intersection types (A & B — combina traits), e opaque types (type-safe sem overhead runtime). Match types computam tipos em tempo de compilação. inline elimina overhead de abstração. Estes features tornam o type system Turing-complete — o mais expressivo entre linguagens mainstream.

Error handling patterns
// 1. Option (ausência sem erro):
def find(id: Int): Option[User] = ???

// 2. Either (erro de domínio):
def parse(s: String): Either[ParseError, Int] = ???

// 3. Try (wrapping exceções Java):
import scala.util.Try
def lerFicheiro(path: String): Try[String] =
  Try(scala.io.Source.fromFile(path).mkString)

// 4. Validated (acumular erros):
import cats.data.ValidatedNel
def validarForm(nome: String, idade: Int)
  : ValidatedNel[String, User] =
  (validarNome(nome), validarIdade(idade)).mapN(User.apply)

// 5. Either combinado:
type AppError = String
type Result[A] = Either[AppError, A]

def processar(input: String): Result[Data] =
  for
    parsed <- parse(input)
    validated <- validate(parsed)
    saved <- save(validated)
  yield saved

Scala oferece múltiplos níveis de error handling: Option (ausência simples), Either (erro tipado de domínio), Try (exceções Java), Validated (acumular múltiplos erros). A escolha depende do contexto: APIs públicas usam Either, wrapping Java usa Try, formulários usam Validated. Todos compõem em for comprehensions.

Controlo de Fluxo


9 cards
If / Else (expressão)
// If é expressão (retorna valor):
val resultado = if idade >= 18 then
  "Adulto"
else
  "Menor"

// If/else if/else:
val msg = if nota >= 18 then "Excelente"
  else if nota >= 10 then "Aprovado"
  else "Reprovado"

// Sintaxe antiga (ainda válida):
val x = if (cond) a else b

// Side effect (Unit):
if x > 0 then
  println("Positivo")

// Tipos diferentes -> Any:
val v: Any = if c then 1 else "a"

// Substitui ternário de outras linguagens:
val status = if ativo then "online" else "offline"

Em Scala, if é uma expressão que retorna valor — substitui o operador ternário. Scala 3 introduz a sintaxe then/else sem parênteses. Se os ramos tiverem tipos diferentes, o resultado é Any. Para side effects puros, o resultado é Unit. Não existe operador ternário ? : — o if expressão cumpre esse papel.

Pattern matching avançado
// Nested patterns:
dados match
  case Some(List(x, y, _*)) => s"$x, $y"
  case Some(Nil) => "lista vazia"
  case None => "nada"

// Type patterns:
def processar(x: Any): String = x match
  case s: String => s"String: $s"
  case n: Int => s"Int: $n"
  case l: List[_] => s"Lista de ${l.size}"
  case (a, b) => s"Par: $a, $b"
  case _ => "outro"

// Variable binding (@):
case lista @ List(_, _, _) =>
  s"Lista de 3: $lista"

// Sealed + exaustivo:
sealed trait Forma
case class Circulo(r: Double) extends Forma
case class Quadrado(l: Double) extends Forma

Padrões aninhados (Some(List(x, y, _*))) decompõem estruturas complexas numa linha — _* captura o resto. Variable binding (@) dá nome ao valor total enquanto faz match na estrutura. sealed trait + pattern matching é o padrão ADT — o compilador verifica exaustividade e avisa se faltar um caso.

Sealed traits e ADTs
// Algebraic Data Types:
sealed trait Expr
case class Num(value: Double) extends Expr
case class Add(l: Expr, r: Expr) extends Expr
case class Mul(l: Expr, r: Expr) extends Expr

// Pattern matching exaustivo:
def eval(e: Expr): Double = e match
  case Num(v) => v
  case Add(l, r) => eval(l) + eval(r)
  case Mul(l, r) => eval(l) * eval(r)

// Uso:
val expr = Add(Num(2), Mul(Num(3), Num(4)))
eval(expr)  // 14.0

// Enum (Scala 3 - ADT simples):
enum Cor:
  case Vermelho, Verde, Azul

enum Forma:
  case Circulo(r: Double)
  case Retangulo(l: Double, a: Double)

sealed trait restringe subclasses ao mesmo ficheiro — o compilador verifica exaustividade no match (avisa se faltar caso). Combinado com case class forma ADTs (Algebraic Data Types) — a base de programação funcional em Scala. enum (Scala 3) é açúcar para hierarquias sealed simples. Ideal para modelar domínios com variantes finitas.

Match / Case
// Pattern matching básico:
val msg = numero match
  case 1 => "um"
  case 2 => "dois"
  case n if n > 10 => "grande"
  case _ => "outro"

// Com tipos:
valor match
  case s: String => s.length
  case n: Int => n * 2
  case _ => 0

// Com case classes:
forma match
  case Circulo(r) => math.Pi * r * r
  case Retangulo(l, a) => l * a

// Com listas:
lista match
  case Nil => "vazia"
  case h :: Nil => s"um: $h"
  case h :: t => s"cabeça: $h"

// Guard clauses:
case n if n % 2 == 0 => "par"

Pattern matching é a feature mais poderosa de Scala — combina switch/case, destructuring e type checking. Com sealed trait, o compilador verifica exaustividade (avisa se faltar caso). Guards (if n > 10) adicionam condições arbitrárias. Padrões de lista (h :: t) decompõem recursivamente — a base de algoritmos funcionais.

Given / Using (Scala 3)
// Given (instância implícita):
given Ordering[Int] with
  def compare(x: Int, y: Int) = x - y

// Using (parâmetro implícito):
def maximo[T](a: T, b: T)(using ord: Ordering[T]): T =
  if ord.compare(a, b) > 0 then a else b

// Extension methods:
extension (s: String)
  def gritar: String = s.toUpperCase + "!"
  def palavras: List[String] = s.split(" ").toList

"olá mundo".gritar   // "OLÁ MUNDO!"
"a b c".palavras     // List("a","b","c")

// Type class com given:
trait Show[T]:
  def show(value: T): String

given Show[Int] with
  def show(value: Int) = s"Int($value)"

given/using (Scala 3) substitui os implicit do Scala 2 — given define instâncias disponíveis, using injeta-as automaticamente. Extension methods adicionam métodos a tipos existentes sem wrapper classes. Juntos, implementam o padrão Type Class: comportamento polimórfico sem herança. Mais claro e seguro que implicit.

For comprehensions
// Básico (side effect):
for i <- 1 to 10 do
  println(i)

// Com yield (retorna coleção):
val quadrados = for i <- 1 to 10 yield i * i

// Com filtro (guard):
val pares = for
  i <- 1 to 20
  if i % 2 == 0
yield i

// Múltiplos geradores:
val coords = for
  x <- 1 to 5
  y <- 1 to 5
yield (x, y)

// Com Option:
val resultado = for
  a <- Some(10)
  b <- Some(20)
yield a + b  // Some(30)

// Equivalente a map/flatMap/filter

For comprehensions são açúcar sintático para map/flatMap/filter — funcionam com qualquer tipo que implemente esses métodos (List, Option, Future, Either). Com yield, retornam coleção; sem yield, executam side effects. Múltiplos geradores produzem produto cartesiano. None/Left faz short-circuit automático.

Try (error handling)
import scala.util.{Try, Success, Failure}

// Try encapsula exceções:
val resultado = Try("42".toInt)
// Success(42)

val erro = Try("abc".toInt)
// Failure(NumberFormatException)

// Pattern matching:
resultado match
  case Success(n) => println(s"Número: $n")
  case Failure(e) => println(s"Erro: $e")

// getOrElse / recover:
resultado.getOrElse(0)
resultado.recover { case _ => 0 }

// map / flatMap:
resultado.map(_ * 2)  // Success(84)

// for comprehension:
val total = for
  a <- Try("10".toInt)
  b <- Try("20".toInt)
yield a + b  // Success(30)

Try encapsula computações que podem lançar exceções — Success(valor) ou Failure(exceção). É a alternativa funcional a try/catch: suporta map, flatMap, for comprehensions e pattern matching. recover trata erros específicos. Para erros de domínio, prefira Either (mais explícito); Try é ideal para wrapping de APIs Java.

While e loops
// While:
var x = 10
while x > 0 do
  println(x)
  x -= 1

// Do-while (Scala 3):
var input = ""
do
  input = readLine()
while input != "sair"

// foreach (preferir a while):
(1 to 10).foreach(println)
List("a","b","c").foreach(println)

// Range com step e direção:
(1 until 10 by 2).foreach(println)
(10 to 1 by -1).foreach(println)

// break (raro, usar recursão):
import scala.util.control.Breaks._
breakable {
  for i <- 1 to 100 do
    if i > 10 then break
}

Em programação funcional, while é desencorajado (requer var mutável). Prefira foreach, map ou recursão. Ranges com by permitem passos customizados e direção inversa. break existe mas é raro — normalmente indica que uma abordagem funcional (takeWhile, find, recursão) seria mais clara e segura.

Either (erros tipados)
// Either: Left (erro) ou Right (sucesso)
type Result[A] = Either[String, A]

def parse(s: String): Result[Int] =
  s.toIntOption match
    case Some(n) => Right(n)
    case None => Left(s"Inválido: $s")

parse("42")   // Right(42)
parse("abc")  // Left("Inválido: abc")

// For comprehension (short-circuit):
val total = for
  a <- parse("10")
  b <- parse("20")
yield a + b  // Right(30)

// Pattern matching:
parse("x") match
  case Right(n) => println(s"OK: $n")
  case Left(msg) => println(s"Erro: $msg")

// Transformar:
parse("42").map(_ * 2)       // Right(84)
parse("x").getOrElse(0)      // 0

Either representa dois resultados possíveis: Left (erro) e Right (sucesso). É o preferido para erros de domínio porque documenta o tipo de erro no signature. For comprehensions com Either fazem short-circuit no primeiro Left. Convenção: Right = sucesso (right = "correct"). Substitui exceções para erros esperados.

Strings e I/O


8 cards
Operações de String
val s = "Olá Mundo"

// Transformações:
s.toUpperCase       // "OLÁ MUNDO"
s.toLowerCase       // "olá mundo"
s.reverse           // "odnuM álO"
s.trim              // remove espaços

// Pesquisa:
s.contains("Mundo")  // true
s.startsWith("Olá")  // true
s.indexOf("Mundo")   // 4
s.matches("Ol.*")    // true (regex)

// Substring e split:
s.substring(4)       // "Mundo"
s.substring(0, 3)    // "Olá"
"a,b,c".split(",")   // Array("a","b","c")

// Substituição:
s.replace("Mundo", "Scala")
"aaa".replaceAll("a", "o")  // "ooo"

// Repetição e padding:
"ab" * 3            // "ababab"
"42".padTo(5, '0')  // "42000"

Strings em Scala herdam todos os métodos de java.lang.String mais extensões. split retorna Array (usar .toList para List). replaceAll aceita regex, replace é literal. matches verifica regex na string inteira. Strings são imutáveis — todas as operações retornam nova string. padTo preenche até comprimento mínimo.

StringBuilder e performance
// StringBuilder (mutável, eficiente):
val sb = new StringBuilder
for i <- 1 to 1000 do
  sb.append(s"linha $i\n")
val resultado = sb.toString

// vs concatenação (lento - O(n²)):
var s = ""
for i <- 1 to 1000 do
  s += s"linha $i\n"  // NOVO objeto a cada iter

// mkString (preferir para coleções):
val csv = List("a", "b", "c").mkString(",")
val html = items.map(li).mkString("<ul>\n", "\n", "\n</ul>")

// String.join (Java):
String.join(", ", "a", "b", "c")

// Comparação de performance:
// mkString > StringBuilder >> concatenação
// Para < 10 elementos, concatenação é OK

Concatenação repetida com + cria novo objeto a cada iteração — O(n²). StringBuilder é mutável e eficiente para construção incremental. mkString é a forma idiomática para coleções (aceita prefixo, separador, sufixo). Para menos de 10 elementos, a diferença é negligenciável. Em hot loops com muitas strings, StringBuilder é essencial.

Interpolação avançada
// s-interpolator:
val nome = "Ana"
val idade = 30
s"$nome tem $idade anos"
s"${nome.toUpperCase}!"  // expressão

// f-interpolator (formatação):
val pi = 3.14159
f"$pi%.2f"        // "3.14"
f"$idade%05d"     // "00030"
f"$pi%10.3f"      // "     3.142"

// raw (sem escapes):
raw"C:\nova\pasta"  // literal

// Interpolador customizado:
implicit class JsonHelper(val sc: StringContext) extends AnyVal:
  def json(args: Any*): String =
    s"""{"value": "${sc.s(args*)}"}"""

json"$nome"  // {"value": "Ana"}

// stripMargin:
val sql = """
  |SELECT *
  |FROM users
  |WHERE age > 18
""".stripMargin

Scala permite interpoladores customizados via implicit class em StringContext — bibliotecas como sql, json, uri usam este mecanismo. f-interpolator suporta toda a formatação printf (%d, %s, %.2f). stripMargin remove prefixo | em strings multilinha. Triple-quotes preservam formatação.

Parsing e conversão
// Parsing seguro (Option):
"42".toIntOption       // Some(42)
"abc".toIntOption      // None
"3.14".toDoubleOption  // Some(3.14)

// Parsing com Try:
import scala.util.Try
Try("42".toInt)        // Success(42)
Try("abc".toInt)       // Failure(...)

// Parsing com Either:
def parseInt(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"Não é número: $s")

// Pipeline de parsing:
val resultado = for
  s <- Some("  42  ")
  trimmed <- Some(s.trim)
  n <- trimmed.toIntOption
yield n * 2  // Some(84)

// BigDecimal (precisão):
val preco = BigDecimal("19.99")
val total = preco * 3  // 59.97 (exato!)

Parsing em Scala deve ser seguro: toIntOption retorna Option (sem exceção), Try captura exceções, Either documenta erros no tipo. Pipelines com for comprehension propagam falhas automaticamente. BigDecimal é obrigatório para valores monetários — Double tem erros de precisão (0.1 + 0.2 != 0.3).

Regex em Scala
import scala.util.matching.Regex

// Criar regex:
val pattern = """(\d+)-(\d+)-(\d+)""".r

// findFirstMatchIn:
pattern.findFirstIn("Data: 25-12-2024")
// Some("25-12-2024")

// findAllIn:
val nums = """\d+""".r
nums.findAllIn("a1 b22 c333").toList
// List("1", "22", "333")

// Pattern matching com regex:
"25-12-2024" match
  case pattern(d, m, a) => s"$a/$m/$d"

// replaceAllIn:
val clean = """\s+""".r.replaceAllIn("a  b   c", " ")

// Regex com grupos nomeados:
val email = """(\w+)@(\w+)\.(\w+)""".r
email.findFirstMatchIn("ana@mail.com") match
  case Some(m) => println(m.group(1))  // "ana"

Regex em Scala usa scala.util.matching.Regex — criado com .r em qualquer string. Pattern matching com regex extrai grupos diretamente (case pattern(d, m, a)). findAllIn retorna iterator de matches. replaceAllIn substitui todas as ocorrências. Triple-quotes evitam escaping de backslashes. Integra-se naturalmente com pattern matching.

Console e argumentos
// Output:
println("Com newline")
print("Sem newline")
Console.err.println("Erro!")

// Formatted output:
printf("%s tem %d anos%n", "Ana", 30)
println(f"$pi%.2f")

// Input:
val nome = scala.io.StdIn.readLine("Nome: ")
val idade = scala.io.StdIn.readInt("Idade: ")

// Argumentos CLI:
@main def app(args: String*): Unit =
  args.foreach(println)
  println(s"Total: ${args.size} args")

// sbt run arg1 arg2
// scala-cli run app.scala -- arg1 arg2

// System properties e env:
sys.props("java.version")
sys.env.getOrElse("HOME", "/tmp")

println/print para output standard. Console.err para stderr. scala.io.StdIn para input do utilizador (readLine, readInt). @main (Scala 3) com varargs recebe argumentos CLI. sys.props acede a system properties, sys.env a variáveis de ambiente. printf usa formatação Java.

File I/O
import scala.io.Source
import java.io.PrintWriter

// Ler ficheiro:
val source = Source.fromFile("dados.txt")
val conteudo = source.mkString
source.close()

// Ler linha-a-linha:
val linhas = Source.fromFile("dados.txt")
  .getLines().toList

// Escrever:
val writer = new PrintWriter("saida.txt")
writer.write("Linha 1\n")
writer.println("Linha 2")
writer.close()

// Using (auto-close, Scala 2.13+):
val texto = Using(Source.fromFile("dados.txt")) { src =>
  src.mkString
}.getOrElse("")

// java.nio (moderno):
import java.nio.file.{Files, Path}
val bytes = Files.readAllBytes(Path.of("dados.txt"))
val content = new String(bytes)
Files.writeString(Path.of("out.txt"), "Olá")

scala.io.Source é a forma clássica de ler ficheiros — sempre fechar com close(). Using (2.13+) fecha automaticamente (try-with-resources). java.nio.file.Files é a API moderna (mais performante para ficheiros grandes). PrintWriter para escrita. Para paths complexos, use os-lib (biblioteca de Li Haoyi).

Char e Unicode
// Char:
val c = 'A'
c.toInt        // 65
c.toLower      // 'a'
c.isLetter     // true
c.isDigit      // false
c.isUpper      // true

// Iterar string (chars):
"Olá".foreach(c => println(c.toInt))

// Unicode:
'\u0041'       // 'A'
"Olá".length   // 4 (UTF-16 units)

// codePoints (Unicode real):
"Olá".codePointCount(0, "Olá".length)  // 4

// Conversões:
65.toChar      // 'A'
('a' to 'z').toList  // alfabeto
('0' to '9').toList  // dígitos

// Character methods:
Character.isWhitespace(' ')  // true
Character.toUpperCase('a')   // 'A'

Char em Scala é UTF-16 (2 bytes). Métodos como isLetter, isDigit, toUpper/toLower são convenientes. Ranges de chars ('a' to 'z') geram sequências. Para Unicode completo (emoji, CJK), use codePointCount e codePoints. String.length conta UTF-16 units, não codepoints.

Patterns e Idioms


8 cards
Pipeline funcional
// Pipeline de transformações:
val resultado = dados
  .map(limpar)
  .filter(validar)
  .map(transformar)
  .foldLeft(resumo)(combinar)

// Pipe operator (Scala 3):
val output = input
  |> trim
  |> toLowerCase
  |> (_.replace(" ", "-"))
  |> slugify

// Composição de funções:
val processar = trim andThen toLowerCase andThen slugify
val resultado2 = processar("  Olá Mundo  ")

// Chaining com tap (side effect):
extension [A](a: A)
  def tap(f: A => Unit): A = { f(a); a }

val user = createUser("Ana")
  .tap(u => log(s"Criado: $u"))
  .tap(u => metrics.increment("users"))

Pipeline encadeia transformações sem estado intermédio — cada passo recebe o resultado do anterior. O pipe operator |> (Scala 3) torna a direção explícita. andThen compõe funções left-to-right. O padrão tap permite side effects no pipeline sem interromper o fluxo. Mais legível e testável que variáveis temporárias.

Type class pattern
// 1. Definir a type class:
trait Encoder[T]:
  def encode(value: T): String

// 2. Instâncias para tipos específicos:
given Encoder[Int] with
  def encode(v: Int) = v.toString

given Encoder[String] with
  def encode(v: String) = s""""$v""""

// 3. Instância derivada:
given [T](using e: Encoder[T]): Encoder[List[T]] with
  def encode(v: List[T]) =
    v.map(e.encode).mkString("[", ",", "]")

// 4. Syntax extension:
extension [T](value: T)(using e: Encoder[T])
  def toJson: String = e.encode(value)

// 5. Uso:
42.toJson            // "42"
"olá".toJson         // ""olá""
List(1,2,3).toJson   // "[1,2,3]"

O Type Class pattern tem 5 passos: definir trait, criar instâncias given, derivar instâncias compostas, adicionar syntax extension, usar. Permite polimorfismo ad-hoc sem herança — tipos existentes ganham comportamento novo sem modificação. É o padrão central em Cats, Circe, http4s. Mais flexível que herança: uma instância por tipo, sem diamond problem.

Builder imutável
// Builder com case class + copy:
case class Request(
  url: String = "",
  method: String = "GET",
  headers: Map[String, String] = Map.empty,
  body: Option[String] = None
):
  def withUrl(u: String) = copy(url = u)
  def withMethod(m: String) = copy(method = m)
  def withHeader(k: String, v: String) =
    copy(headers = headers + (k -> v))
  def withBody(b: String) = copy(body = Some(b))

// Uso (fluent API):
val req = Request()
  .withUrl("https://api.com")
  .withMethod("POST")
  .withHeader("Auth", "token")
  .withBody("""{"key": "value"}""")

// Cada with* retorna NOVA instância
// Thread-safe, sem mutação

Builder imutável usa case class + copy — cada with* retorna nova instância (thread-safe, sem mutação). Default args tornam todos os campos opcionais. Mais seguro que Builder mutável (GoF) — impossível estado inconsistente. O padrão é ubíquo em bibliotecas Scala (Akka, http4s, doobie). Fluent API sem side effects.

Error accumulation
import cats.data.ValidatedNel
import cats.syntax.all._

type Errors[A] = ValidatedNel[String, A]

// Validações independentes:
def validarNome(s: String): Errors[String] =
  if s.nonEmpty then s.validNel
  else "Nome vazio".invalidNel

def validarIdade(n: Int): Errors[Int] =
  if n >= 0 && n <= 150 then n.validNel
  else s"Idade inválida: $n".invalidNel

def validarEmail(s: String): Errors[String] =
  if s.contains("@") then s.validNel
  else "Email sem @".invalidNel

// Acumular TODOS os erros:
case class User(nome: String, idade: Int, email: String)

def validarForm(nome: String, idade: Int, email: String)
  : Errors[User] =
  (validarNome(nome), validarIdade(idade), validarEmail(email))
    .mapN(User.apply)

// Either faz short-circuit (só 1º erro)
// Validated acumula todos

Validated (Cats) acumula TODOS os erros de validação — diferente de Either que para no primeiro. ValidatedNel = Validated[NonEmptyList[E], A]. mapN combina N validações e aplica a função se todas passarem. Ideal para formulários (mostrar todos os erros de uma vez). invalidNel/validNel são syntax sugar.

Strategy com funções
// Strategy = passar funções:
type Strategy = List[Int] => List[Int]
val ascending: Strategy = _.sorted
val descending: Strategy = _.sorted.reverse
val evensFirst: Strategy = xs =>
  xs.filter(_ % 2 == 0) ++ xs.filter(_ % 2 == 1)

def ordenar(dados: List[Int], s: Strategy) = s(dados)
ordenar(List(3,1,4,1,5), ascending)
ordenar(List(3,1,4,1,5), descending)

// Comparator como função:
case class User(nome: String, idade: Int)
val users = List(User("Ana", 30), User("Rui", 25))

users.sortBy(_.idade)
users.sortBy(_.nome)
users.sortWith((a, b) => a.idade > b.idade)

// Higher-order strategy:
def retry[T](n: Int)(f: () => T): T =
  Try(f()).getOrElse(retry(n - 1)(f))

Strategy pattern em FP é simplesmente passar funções como parâmetros — type alias para clareza. Elimina hierarquias de classes/traits para comportamento variável. sortBy/sortWith aceitam estratégias de ordenação como funções. Higher-order strategies (retry) encapsulam padrões de execução. Mais conciso e composável que OOP.

State machine pattern
// Estados como ADT:
sealed trait DoorState
case object Closed extends DoorState
case object Open extends DoorState
case object Locked extends DoorState

// Eventos:
sealed trait DoorEvent
case object Push extends DoorEvent
case object Pull extends DoorEvent
case object Lock extends DoorEvent
case object Unlock extends DoorEvent

// Transição (função pura):
def transition(state: DoorState, event: DoorEvent): DoorState =
  (state, event) match
    case (Closed, Push) => Open
    case (Open, Pull) => Closed
    case (Closed, Lock) => Locked
    case (Locked, Unlock) => Closed
    case _ => state  // transição inválida

// Executar sequência:
val events = List(Push, Pull, Lock, Unlock)
val final_state = events.foldLeft(Closed: DoorState)(transition)

State machines em FP são funções puras (State, Event) => State — sem mutação, facilmente testável. Pattern matching com tuplo (state, event) define todas as transições. foldLeft executa sequências de eventos. Transições inválidas retornam o estado atual (ou Either para logging). Usado em Akka FSM, http4s middleware e UI state.

ADTs e modelagem
// Modelar domínio com ADTs:
sealed trait Payment
case class Card(number: String, exp: String) extends Payment
case class BankAccount(iban: String) extends Payment
case object Cash extends Payment

// Processamento exaustivo:
def processar(p: Payment): String = p match
  case Card(num, _) => s"Cartão: ${num.takeRight(4)}"
  case BankAccount(iban) => s"IBAN: $iban"
  case Cash => "Dinheiro"

// Estado como ADT:
sealed trait State
case object Loading extends State
case class Loaded(data: List[String]) extends State
case class Error(msg: String) extends State

// Transições:
def transition(s: State, event: Event): State =
  (s, event) match
    case (Loading, DataReady(d)) => Loaded(d)
    case (Loading, Failed(e)) => Error(e)
    case _ => s

ADTs (Algebraic Data Types) modelam domínios com variantes finitas — sealed trait + case class/case object. O compilador verifica exaustividade (impossível esquecer um caso). Pattern matching com tuplos (state, event) modela state machines. Substitui hierarquias OOP complexas com código mais simples e seguro. Padrão central em Scala.

Idioms e convenções
// 1. Preferir val sobre var
// 2. Preferir imutável sobre mutável
// 3. Option em vez de null
// 4. Either em vez de exceções (domínio)
// 5. Pattern matching em vez de if/else chains

// Named args para clareza:
connect(host = "localhost", port = 5432)

// Default args em vez de overloads:
def log(msg: String, level: String = "INFO") = ???

// Case class em vez de Tuple com significado:
case class Point(x: Double, y: Double)

// Extension em vez de utility classes:
extension (s: String) def slug: String = ???

// for comprehension em vez de flatMap chains:
val r = for
  a <- fetchA()
  b <- fetchB(a)
yield combine(a, b)

// sealed + match em vez de visitor pattern

Idioms de Scala: val por defeito, imutável por defeito, Option em vez de null, Either em vez de exceções para erros esperados. Named/default args eliminam overloads. Case classes documentam melhor que tuples. Extension methods substituem utility classes. For comprehensions são mais legíveis que flatMap chains. sealed + match substitui visitor pattern.