DevTools

Cheatsheet Scala

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

Volver a los lenguajes
Scala
72 tarjetas encontradas
Categorías:
Versiones:

Básico e Tipos


10 cards
Variables (val y var)
// val (inmutable - preferir siempre):
val nombre = "Ana"
val edad: Int = 30

// var (mutable - usar con moderación):
var contador = 0
contador += 1

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

// Lazy evaluation:
lazy val datos = cargarDatos()

// Múltiple asignación (destructuring):
val (a, b) = (1, 2)
val (x, y, z) = (10, 20, 30)

Usa val por defecto (inmutable) — el compilador impide la reasignación y permite optimizaciones. var solo cuando el estado necesita cambiar. lazy val retrasa la evaluación hasta el primer acceso — ideal para recursos caros. La type inference de Scala es fuerte — raramente necesitas anotar el tipo.

Tuples
// Crear:
val par = (1, "uno")
val triple = (1, "uno", true)
val punto = (3.0, 4.0)

// Acceso por posición:
par._1  // 1
par._2  // "uno"

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

// Retorno múltiple de funciones:
def dividir(a: Int, b: Int): (Int, Int) =
  (a / b, a % b)
val (q, r) = dividir(10, 3)

// zip (combinar colecciones):
val pares = List(1,2,3).zip(List("a","b","c"))
// List((1,"a"), (2,"b"), (3,"c"))

Las tuples agrupan valores heterogéneos sin crear una clase — ideales para retornar múltiples valores. Acceso por posición (_1, _2) o destructuring en la asignación. zip combina dos colecciones en pares. Para datos con significado semántico, prefiere case class — las tuples no documentan qué representa cada posición.

Any, Nothing y Unit
// Any (top type - súper de todo):
val cualquiera: Any = 42
cualquiera = "texto"  // OK
cualquiera = List(1)  // OK

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

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

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

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

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

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

Any es el top type (supertipo de todo). Nothing es el bottom type (subtipo de todo) — las funciones que devuelven Nothing nunca completan normalmente (throw, sys.exit). Unit indica ausencia de valor útil (como void) pero es un tipo real con valor (). Esta jerarquía permite polimorfismo total.

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

// Otros:
Boolean, Char, String, Unit

// Literales:
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

// Jerarquía:
// Any -> AnyVal (primitivos)
// Any -> AnyRef (objetos = Java Object)

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

En Scala, todo es objeto — Int, Boolean tienen métodos (42.toString). Any es la raíz de la jerarquía (AnyRef = objetos, AnyVal = primitivos). Nothing es el bottom type (subtipo de todo) — usado para funciones que nunca retornan normalmente. Unit equivale al void pero es un tipo real con valor ().

Conversiones 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

// Conversión segura (Option):
"42".toIntOption   // Some(42)
"abc".toIntOption  // None

// BigDecimal (precisión):
BigDecimal("3.14159")
BigDecimal(1) / BigDecimal(3)

// Formateo:
"%05d".format(42)   // "00042"
"%.2f".format(pi)   // "3.14"

Las conversiones en Scala son explícitas vía métodos toX (toInt, toDouble). toInt lanza excepción si es inválido — usa toIntOption para conversión segura que devuelve Option. BigDecimal es esencial para valores monetarios (Double tiene errores de precisión). format usa sintaxis printf.

Type inference y anotaciones
// Inferencia automática:
val x = 42          // Int
val s = "hola"      // String
val l = List(1,2,3) // List[Int]

// Cuándo anotar (necesario):
def sumar(a: Int, b: Int): Int = a + b
val f: Int => Int = _ * 2

// Inferencia en expresiones:
val resultado = if true then 1 else 2  // Int
val msg = if false then "a" else 1     // Any

// Tipos en colecciones:
val nums: List[Int] = List(1, 2, 3)
val map: Map[String, Int] = Map("a" -> 1)

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

// isInstanceOf (verificar):
if cualquiera.isInstanceOf[String] then
  println("es string")

La type inference de Scala es muy fuerte — raramente necesitas anotar. Las anotaciones son necesarias en parámetros de funciones, tipos de retorno públicos (buena práctica) y cuando el compilador no puede inferir. asInstanceOf hace cast unchecked (evitar — preferir pattern matching). isInstanceOf verifica el tipo en runtime. Prefiere match con type patterns.

Strings e interpolación
// Interpolación s:
val nombre = "Mundo"
s"¡Hola, $nombre!"   // "¡Hola, Mundo!"
s"2+2 = ${2 + 2}"    // "2+2 = 4"

// Interpolación f (formateo):
f"$pi%.2f"           // "3.14"
f"$n%05d"            // "00042"

// Raw (sin escapes):
raw"linea1\nlinea2"  // literal \n

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

// Operaciones:
"hola".toUpperCase   // "HOLA"
"a,b,c".split(",")   // Array("a","b","c")
"abc" * 3            // "abcabcabc"
" abc ".trim         // "abc"

Scala tiene tres interpoladores: s (sustitución de variables), f (formateo printf-style como %.2f) y raw (sin procesar escapes). Las triple-quoted strings (\"\"\") preservan el formato multilínea — stripMargin elimina el prefijo |. Todas las operaciones de String de Java están disponibles.

Option (null safety)
// Option encapsula un valor opcional:
val nombre: Option[String] = Some("Ana")
val vacio: Option[String] = None

// getOrElse (default seguro):
nombre.getOrElse("Anónimo")  // "Ana"
vacio.getOrElse("Anónimo")   // "Anónimo"

// map/flatMap (propaga None):
nombre.map(_.toUpperCase)    // Some("ANA")
vacio.map(_.toUpperCase)     // None

// Pattern matching:
nombre match
  case Some(n) => println(n)
  case None    => println("Vacío")

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

// Convención: nunca usar null
// Las APIs devuelven Option

Option es la alternativa de Scala al null — encapsula un valor que puede o no existir (Some/None). map/flatMap propagan None automáticamente (sin NullPointerException). getOrElse proporciona un default seguro. La convención en Scala es nunca usar null — las APIs bien diseñadas devuelven Option.

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

// Comparación:
== != > < >= <=
// == compara VALORES (¡no referencias!)

// 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)

// Los operadores son métodos:
1 + 2 == 1.+(2)
"abc".reverse == "cba"

// Asignación compuesta:
+= -= *= /= %=

En Scala, los operadores son métodos — 1 + 2 es azúcar para 1.+(2). Puedes definir operadores personalizados en tus clases. == compara valores (usa equals), no referencias (a diferencia de Java). Los ranges (to = inclusivo, until = exclusivo, by = step) se usan extensivamente en for loops.

Rangos y secuencias
// Range inclusivo:
val r1 = 1 to 10      // Range(1,2,...,10)

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

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

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

// Operaciones en 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 pertenencia:
(1 to 10).contains(5)  // true

Los ranges son secuencias lazy de enteros — to es inclusivo, until exclusivo, by define el step. Funcionan con chars ('a' to 'z'). Soportan todas las operaciones de colecciones (map, filter, sum). Son la forma idiomática de iterar en Scala — sustituyen los loops indexados de lenguajes imperativos.

Funções


9 cards
Definir funciones
// def (método):
def sumar(a: Int, b: Int): Int = a + b

// Multi-línea (Scala 3 indentación):
def factorial(n: Int): Int =
  if n <= 1 then 1
  else n * factorial(n - 1)

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

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

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

def define métodos (evaluados en cada llamada). El tipo de retorno puede inferirse en expresiones simples. Los default args eliminan sobrecargas. Los Varargs (Int*) aceptan N argumentos. Los named args (puerto = 5432) mejoran la legibilidad. En Scala 3, la indentación define bloques (sin llaves).

Recursión y @tailrec
// Recursión simple:
def factorial(n: Int): BigInt =
  if n <= 1 then 1
  else n * factorial(n - 1)

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

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

// Fibonacci con 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 (secuencias infinitas):
lazy val fibs: LazyList[BigInt] =
  BigInt(0) #:: BigInt(1) #::
  fibs.zip(fibs.tail).map(_ + _)

fibs.take(10).toList

La recursión sustituye los loops imperativos en FP. @tailrec garantiza que el compilador optimiza a loop (sin stack overflow) — si no es tail-recursive, da error de compilación. El acumulador (acc) lleva el resultado parcial. LazyList permite secuencias infinitas — solo calcula los elementos necesarios (take(10) no calcula el resto).

Funciones inline y transparent
// inline (elimina overhead en compilación):
inline def max(inline a: Int, inline b: Int): Int =
  if a > b then a else b

// Sin overhead de llamada:
val x = max(10, 20)  // compilado como if directo

// 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 tamano[T]: Int =
  constValue[Tuple.Size[T]]

inline (Scala 3) elimina el overhead de abstracción — el código se expande en el call site en compilación. transparent inline permite que el compilador infiera el tipo preciso (no el tipo declarado). inline if se resuelve en compilación. Esencial para bibliotecas de alto rendimiento y type-level programming. Coste runtime cero para abstracciones.

Lambdas y funciones anónimas
// Sintaxis completa:
val suma = (a: Int, b: Int) => a + b

// Type inference:
val doble = (x: Int) => x * 2
val doble2: Int => Int = _ * 2

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

// Sin parámetros:
val ahora = () => System.currentTimeMillis()

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

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

Las lambdas son valores de primera clase en Scala. El underscore (_) es placeholder para el parámetro — _ * 2 equivale a x => x * 2. Cada _ representa un parámetro diferente (_ + _ = (a,b) => a + b). La sintaxis placeholder hace las operaciones en colecciones extremadamente concisas.

Funciones como valores
// Asignar a val:
val doble: Int => Int = _ * 2
doble(5)  // 10

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

// Devolver función (closure):
def multiplicador(factor: Int): Int => Int =
  _ * factor
val triple = multiplicador(3)
triple(5)  // 15

// Closure con 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

Las funciones son valores de primera clase — pueden almacenarse, pasarse, devolverse y colocarse en colecciones. Int => Int es azúcar para Function1[Int, Int]. Las closures capturan variables del entorno (contador mantiene estado). Las fábricas de funciones (multiplicador) crean comportamiento parametrizado sin clases.

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

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

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

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

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

// Composición de funciones:
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)

Las HOFs reciben o devuelven funciones — la base de la programación funcional. map transforma, filter selecciona, fold/reduce agregan. flatMap mapea y aplana. compose/andThen componen funciones (f∘g vs g∘f). Estas operaciones se encadenan en pipelines sin variables mutables.

Métodos vs Funciones
// Método (def):
def método(x: Int): Int = x * 2

// Función (val):
val funcion: Int => Int = x => x * 2

// Eta expansion (método -> función):
val f = método _   // Int => Int
val g = método     // Scala 3: auto-convierte

// Diferencias:
// - def se evalúa en cada llamada
// - val se evalúa una vez
// - def puede tener type params
// - val es objeto (Function1)

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

repetir(3) {
  println("Hola")  // ejecutado 3x
}

def es un método (evaluado en cada llamada, puede tener type parameters, no es un valor). Un val con tipo función es un objeto Function1 (evaluado una vez, puede pasarse). Los by-name parameters (=> Unit) retrasan la evaluación del argumento — el bloque se reevalúa en cada uso (esencial para DSLs y control de flujo personalizado).

Currying y partial application
// Currying (múltiples listas de params):
def suma(a: Int)(b: Int): Int = a + b
suma(3)(4)  // 7

// Partial application:
val suma3 = suma(3) _
suma3(4)   // 7
suma3(10)  // 13

// Múltiples parámetros:
def formatear(prefix: String)(valor: Int)(sufijo: String) =
  s"$prefix$valor$sufijo"

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

// fold con 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

El currying transforma una función de N parámetros en N funciones de 1 parámetro — permite partial application (fijar argumentos). Las múltiples listas de parámetros son idiomáticas (foldLeft(0)(_ + _)). El pipe operator |> (Scala 3) encadena transformaciones de izquierda a derecha — más legible que la composición anidada.

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

pf(1)          // "uno"
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 (componer parciales):
val par: PartialFunction[Int, String] =
  case n if n % 2 == 0 => "par"
val impar: PartialFunction[Int, String] =
  case n if n % 2 == 1 => "impar"
val todos = par orElse impar

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

PartialFunction solo está definida para ciertos inputs — usada en pattern matching y collect. isDefinedAt verifica si el input es válido. orElse compone parciales (fallback). andThen transforma el resultado. Esencial en actores (Akka) y routing. Diferente de Function1: puede no estar definida para todos los inputs del tipo.

Classes e Traits


9 cards
Clases
class Persona(val nombre: String, var edad: Int):
  def saludo: String = s"Hola, soy $nombre"
  def envejecer(): Unit = edad += 1

// Uso:
val p = Persona("Ana", 30)
p.nombre     // "Ana" (val = getter)
p.edad = 31  // OK (var = setter)
p.saludo

// Constructor auxiliar:
class Punto(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"Punto($x, $y)"

Los parámetros con val/var en el constructor se convierten en campos automáticamente (sin boilerplate). val = solo getter (inmutable), var = getter + setter. Los constructores auxiliares (def this) deben llamar al primario. El private constructor fuerza el uso de factory methods. Scala 3 usa indentación en vez de llaves.

Generics
// Clase genérica:
class Caja[T](val contenido: T):
  def map[U](f: T => U): Caja[U] =
    Caja(f(contenido))

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

// Bounds:
def máximo[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 tamano(l: List[_]): Int = l.size

Los generics en Scala son más expresivos que en Java: +T (covariante — Caja[Dog] es subtipo de Caja[Animal]), -T (contravariante), T (invariante). El upper bound (<:) exige un subtipo, el context bound (T: Ordering) exige una instancia given. Las variance annotations las verifica el compilador.

Opaque types y value classes
// Opaque type (type-safe, cero 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)  // ¡ERROR! Int != UserId

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

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

Los opaque types (Scala 3) crean tipos distintos sin overhead en runtime — UserId es Int en runtime pero un tipo separado en compilación. Previenen errores de mezclar valores (UserId vs OrderId). Las value classes (extends AnyVal) son la alternativa de Scala 2. Ambos eliminan la clase de bugs "primitive obsession" con coste cero.

Case classes
case class User(nombre: String, edad: Int, email: String)

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

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

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

// Sealed hierarchy (ADT):
sealed trait Forma
case class Círculo(radio: Double) extends Forma
case class Rectangulo(l: Double, a: Double) extends Forma
case object Punto extends Forma

def area(f: Forma): Double = f match
  case Círculo(r) => math.Pi * r * r
  case Rectangulo(l, a) => l * a
  case Punto => 0

case class genera automáticamente equals, hashCode, toString, copy y unapply (para pattern matching). copy crea una copia con campos alterados — esencial para actualización inmutable. sealed trait + case classes forman ADTs — el compilador verifica la exhaustividad en el match.

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

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

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

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

// Usar con 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]"

Las type classes permiten polimorfismo ad-hoc sin herencia — define el comportamiento (trait), crea instancias para tipos específicos (given) y úsalas con context bounds. La instancia para List[T] se deriva automáticamente si existe instancia para T. summon[T] obtiene la instancia given en scope. Patrón central en Cats y Circe.

Traits y mixins
// Trait (interfaz + implementación):
trait Nadador:
  def nadar(): String = "Nadando"

trait Volador:
  def volar(): String

// Implementar múltiples:
class Pato extends Nadador, Volador:
  def volar(): String = "Volando (bajo)"

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

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

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

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

Las traits son más poderosas que las interfaces — pueden tener implementación, estado y componerse vía mixins. Las stackable traits permiten componer comportamientos en cadena (Upper + Exclaim). El orden del mixin importa — super llama a la trait anterior en la cadena (linearización). Una clase puede extender múltiples traits (sin diamond problem).

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

AppConfig.iniciar()

// Companion object:
class User private (val nombre: String, val edad: Int)

object User:
  // Factory method:
  def apply(nombre: String, edad: Int): User =
    new User(nombre, edad)

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

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

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

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

object define singletons (una instancia por JVM). El companion object comparte nombre con una clase y tiene acceso mutuo a los miembros privados. apply() es el factory method convencional — permite User("Ana") sin new. unapply() habilita pattern matching y destructuring. @main (Scala 3) define puntos de entrada sin object con def main.

Herencia y abstract classes
// Abstract class:
abstract class Animal(val nombre: String):
  def sonido(): String
  def descripcion: String = s"$nombre hace ${sonido()}"

class Perro(nombre: String) extends Animal(nombre):
  def sonido(): String = "¡Guau!"

class Gato(nombre: String) extends Animal(nombre):
  def sonido(): String = "¡Miau!"

// Final y sealed:
final class Inmutable   // no puede ser extendida
sealed trait Expr       // hijos en el mismo fichero

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

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

Scala soporta herencia simple (extends) con abstract class para código compartido. sealed restringe las subclases al mismo fichero — esencial para pattern matching exhaustivo. object define singletons. El companion object (mismo nombre que la clase) define factory methods (apply permite User("Ana") sin new). ??? lanza NotImplementedError.

Enum (Scala 3)
// Enum simple:
enum Color:
  case Rojo, Verde, Azul

// Enum con parámetros:
enum Planet(val masa: Double, val radio: Double):
  case Mercurio extends Planet(3.303e+23, 2.4397e6)
  case Tierra extends Planet(5.976e+24, 6.37814e6)

  def gravedad: Double =
    6.67300E-11 * masa / (radio * radio)

// Enum ADT (con case classes):
enum Forma:
  case Círculo(r: Double)
  case Rectangulo(l: Double, a: Double)

// Pattern matching:
def describir(c: Color): String = c match
  case Color.Rojo => "caliente"
  case Color.Verde => "neutro"
  case Color.Azul => "frío"

// values y valueOf:
Color.values       // Array(Rojo, Verde, Azul)
Color.valueOf("Verde")

enum (Scala 3) es azúcar para sealed trait + case object/case class. Soporta parámetros, métodos y pattern matching exhaustivo. values lista todos los casos, valueOf convierte un string. Los enums con parámetros (como Planet) sustituyen patrones verbosos de Scala 2. Ideal para conjuntos finitos de variantes.

Coleções


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

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

// Construcción:
0 :: lista       // prepend O(1)
lista :+ 6       // append O(n)
lista ++ List(6, 7)  // concat

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

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

List es una linked list inmutable — prepend (::) es O(1), el acceso por índice es O(n). Ideal para recursión y cuando siempre añades a la cabeza. Vector ofrece acceso O(1) a cualquier índice (árbol trie). head/tail descomponen para pattern matching. Todas las colecciones son inmutables por defecto.

Ordenación y transformación
val personas = List(
  ("Ana", 30), ("Ray", 25), ("Eva", 35)
)

// Ordenar:
personas.sortBy(_._2)   // por edad (asc)
personas.sortBy(_._1)   // por nombre
personas.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 con intermedios):
List(1,2,3).scanLeft(0)(_ + _)  // List(0,1,3,6)

sortBy ordena por clave extraída (ascendente); sortWith usa un comparador personalizado. sliding crea ventanas superpuestas (medias móviles), grouped divide en bloques (paginación). flatten aplana colecciones anidadas, transpose intercambia filas por columnas. scanLeft retorna todos los resultados intermedios del fold.

groupBy y transformaciones
val palabras = List("gato", "perro", "gallina", "pato")

// groupBy:
palabras.groupBy(_.head)
// Map(g -> List(gato, gallina), p -> List(perro, pato))

// Contar frecuencias:
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, "Ray" -> 87)
scores.map((k, v) => (k, v + 5))

// flatMap con 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 clave retornando Map[K, List[V]]. flatMap con toIntOption filtra y convierte en una operación (ignora None). unzip separa una lista de tuplas en dos listas. mkString junta elementos con separador. Estas operaciones se combinan para transformaciones complejas de datos de forma declarativa.

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

// Acceso:
scores("Ana")              // 95 (throw si no existe)
scores.getOrElse("Eva", 0) // 0
scores.get("Ana")          // Some(95)
scores.contains("Ana")     // true

// Modificar (inmutable = nuevo Map):
val nuevo = scores + ("Eva" -> 100)
val sin = scores - "Ray"
val merged = scores ++ Map("Nuevo" -> 50)

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

// Set:
val colores = Set("red", "green", "blue")
colores + "yellow"
colores.contains("red")      // true
Set(1,2,3) & Set(2,3,4)     // intersección
Set(1,2,3) | Set(3,4,5)     // unión

El Map inmutable retorna un nuevo Map en cada operación (+, -, ++) — el original nunca cambia. get() retorna Option (seguro), apply() lanza excepción si la clave no existe. Set garantiza unicidad y soporta operaciones matemáticas (& intersección, | unión). El destructuring (nombre, score) <- scores es idiomático.

LazyList y views
// LazyList (evaluación perezosa):
lazy val naturales: LazyList[Int] =
  1 #:: naturales.map(_ + 1)

naturales.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 (sin colecciones intermedias):
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 evalúa elementos bajo demanda — permite secuencias infinitas sin memoria infinita. view evita colecciones intermedias en cadenas de operaciones (procesa elemento a elemento). Iterator es single-use (no puede re-iterar). Usa view para performance en colecciones grandes con múltiples transformaciones. #:: es el constructor lazy (como :: para List).

zip, sliding y combinadores
// zip (combinar en 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 (con default):
List(1,2).zipAll(List("a","b","c"), 0, "?")

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

// grouped (bloques):
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 dos colecciones en pares (para en la más corta). zipWithIndex añade índices. sliding crea ventanas superpuestas (medias móviles), grouped divide en bloques fijos. combinations/permutations generan combinaciones matemáticas. tabulate crea una lista a partir de una función del índice. Todos retornan iterators lazy.

Operaciones funcionales
val nums = List(1, 2, 3, 4, 5, 6, 7, 8)

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

// Agregación:
nums.reduce(_ + _)    // 36
nums.fold(0)(_ + _)   // 36
nums.sum              // 36
nums.count(_ % 2 == 0) // 4

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

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

collect combina filter + map con pattern matching. foldLeft acumula desde la izquierda (el orden importa). find retorna Option. groupBy agrupa por clave (retorna Map). partition divide en dos grupos (tupla de colecciones). Estas operaciones se encadenan en pipelines funcionales sin variables mutables.

Array y colecciones mutables
// Array (mutable, tamaño fijo):
val arr = Array(1, 2, 3, 4, 5)
arr(0) = 10  // ¡mutable!
arr.length   // 5

// ArrayBuffer (tamaño variable):
import scala.collection.mutable.ArrayBuffer
val buf = ArrayBuffer(1, 2, 3)
buf += 4
buf ++= List(5, 6)
buf.remove(0)

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

// Convertir:
val inmutable = buf.toList
val mutable2 = lista.to(ArrayBuffer)

// Convención: inmutable por defecto

Array es el array de Java con tamaño fijo y mutación in-place. ArrayBuffer crece dinámicamente (como ArrayList). Las colecciones mutables (mutable.Map, mutable.Set) modifican in-place — más performantes pero menos seguras en código concurrente. La convención es usar inmutables por defecto y mutables solo en 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 (error 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 (excepción capturada):
import scala.util.Try
Try("42".toInt)   // Success(42)
Try("abc".toInt)  // Failure(...)

Tres tipos para representar fallo: Option (valor ausente, sin info de error), Either (error explícito — Left=error, Right=éxito), Try (excepción capturada). Either es el preferido para errores de dominio porque documenta el tipo de error. Todos funcionan en for comprehensions — None/Left/Failure hace short-circuit.

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

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

// fold (con 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 intermedios):
nums.scanLeft(0)(_ + _)   // List(0,1,3,6,10,15)

// Aplicaciones:
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 sin valor inicial (falla en lista vacía). fold acepta seed (seguro en vacías). foldLeft procesa desde la izquierda (eficiente en List), foldRight desde la derecha (el orden importa para operaciones no conmutativas). scanLeft retorna todos los pasos intermedios. fold es la operación más general — map/filter pueden expresarse con fold.

Avançado


9 cards
Futures y concurrencia
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

// Crear 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)

// Secuencia 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 un valor calculado asíncronamente. map/filter/recover transforman sin bloquear. La for comprehension con Futures ejecuta en secuencia. Future.sequence convierte List[Future[A]] en Future[List[A]]. Await.result bloquea la thread — evitar en producción. recover trata fallos (como catch).

SBT y proyecto
// build.sbt:
name := "mi-proyecto"
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)

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

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

// Ammonite (REPL avanzado):
// amm

SBT (Scala Build Tool) es el build estándar — gestiona compilación, dependencias, tests y publicación. %% añade la versión de Scala automáticamente al artifact. % Test marca dependencias solo para tests. scala-cli es la herramienta moderna para scripts rápidos (single-file). sbt assembly genera un fat JAR para deployment.

Testing con 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 es el framework de tests estándar — AnyFlatSpec con Matchers proporciona un DSL legible (should be, shouldEqual). Soporta múltiples estilos (FlatSpec, FunSuite, WordSpec). ScalaCheck añade property-based testing (genera inputs aleatorios y verifica invariantes). Los tests se ejecutan con sbt test.

Implicits y 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 (obtener instancia):
val s = summon[Show[Int]]

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

given/using es el mecanismo de resolución implícita de Scala 3 — el compilador búsqueda automáticamente instancias given en scope. Permite type classes sin passing manual de dependencias. summon[T] obtiene la instancia explícitamente. Los extension methods con given añaden métodos condicionales. Sustituye los implicit confusos de Scala 2.

Cats y librerías FP
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 errores):
import cats.data.ValidatedNel
type Errors[A] = ValidatedNel[String, A]

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

// IO (Cats Effect):
import cats.effect.IO
val programa: IO[Unit] = for
  _ <- IO.println("¿Nombre?")
  nombre <- IO.readLine
  _ <- IO.println(s"¡Hola, $nombre!")
yield ()

Cats proporciona type classes (Functor, Monad, Applicative) y operaciones universales. mapN combina N Options/Eithers sin for comprehension. traverse convierte List[Option[A]] en Option[List[A]]. Validated acumula todos los errores (Either hace short-circuit en el primero). Cats Effect IO gestiona efectos secundarios de forma pura y testeable.

Monads y 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.nombre}: ${posts.size} posts"

Un Monad es cualquier tipo con map y flatMapOption, Either, Future, List, Try. La for comprehension es azúcar para flatMap/map encadenados (cada <- es un flatMap, yield es map). Si algún paso retorna None/Left/Failure, los siguientes se ignoran (short-circuit).

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

"hola mundo".gritar      // "HOLA MUNDO!"
"a b c".palabras         // List("a","b","c")
"texto largo".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 con given:
extension [T](value: T)(using s: Show[T])
  def showStr: String = s.show(value)

Los extension methods (Scala 3) añaden métodos a tipos existentes sin wrapper classes ni herencia. Pueden ser genéricos ([T]) y condicionales (using). Sustituyen las implicit classes de Scala 2 con sintaxis más clara. Ideales para utilidades en String, List y tipos de dominio. El método se resuelve estáticamente — cero overhead en runtime.

Sistema de tipos avanzado
// Union types (Scala 3):
def procesar(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 introduce union types (A | B — sustituye overloads), intersection types (A & B — combina traits) y opaque types (type-safe sin overhead en runtime). Los match types computan tipos en tiempo de compilación. inline elimina el overhead de abstracción. Estas features hacen el type system Turing-complete — el más expresivo entre lenguajes mainstream.

Error handling patterns
// 1. Option (ausencia sin error):
def find(id: Int): Option[User] = ???

// 2. Either (error de dominio):
def parse(s: String): Either[ParseError, Int] = ???

// 3. Try (wrapping excepciones Java):
import scala.util.Try
def leerFichero(path: String): Try[String] =
  Try(scala.io.Source.fromFile(path).mkString)

// 4. Validated (acumular errores):
import cats.data.ValidatedNel
def validarForm(nombre: String, edad: Int)
  : ValidatedNel[String, User] =
  (validarNombre(nombre), validarEdad(edad)).mapN(User.apply)

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

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

Scala ofrece múltiples niveles de error handling: Option (ausencia simple), Either (error tipado de dominio), Try (excepciones Java), Validated (acumular múltiples errores). La elección depende del contexto: las APIs públicas usan Either, el wrapping de Java usa Try, los formularios usan Validated. Todos componen en for comprehensions.

Controlo de Fluxo


9 cards
If / Else (expresión)
// If es expresión (devuelve valor):
val resultado = if edad >= 18 then
  "Adulto"
else
  "Menor"

// If/else if/else:
val msg = if nota >= 18 then "Excelente"
  else if nota >= 10 then "Aprobado"
  else "Suspendido"

// Sintaxis antigua (aún 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"

// Sustituye al ternario de otros lenguajes:
val status = if activo then "online" else "offline"

En Scala, if es una expresión que devuelve valor — sustituye al operador ternario. Scala 3 introduce la sintaxis then/else sin paréntesis. Si las ramas tienen tipos diferentes, el resultado es Any. Para side effects puros, el resultado es Unit. No existe operador ternario ? : — el if expresión cumple ese papel.

Pattern matching avanzado
// Nested patterns:
datos match
  case Some(List(x, y, _*)) => s"$x, $y"
  case Some(Nil) => "lista vacía"
  case None => "nada"

// Type patterns:
def procesar(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 _ => "otro"

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

// Sealed + exhaustivo:
sealed trait Forma
case class Círculo(r: Double) extends Forma
case class Cuadrado(l: Double) extends Forma

Los patrones anidados (Some(List(x, y, _*))) descomponen estructuras complejas en una línea — _* captura el resto. El variable binding (@) da nombre al valor total mientras hace match en la estructura. sealed trait + pattern matching es el patrón ADT — el compilador verifica la exhaustividad y avisa si falta un caso.

Sealed traits y 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 exhaustivo:
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 simple):
enum Color:
  case Rojo, Verde, Azul

enum Forma:
  case Círculo(r: Double)
  case Rectangulo(l: Double, a: Double)

sealed trait restringe las subclases al mismo fichero — el compilador verifica la exhaustividad en el match (avisa si falta un caso). Combinado con case class forma ADTs (Algebraic Data Types) — la base de la programación funcional en Scala. enum (Scala 3) es azúcar para jerarquías sealed simples. Ideal para modelar dominios con variantes finitas.

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

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

// Con case classes:
forma match
  case Círculo(r) => math.Pi * r * r
  case Rectangulo(l, a) => l * a

// Con listas:
lista match
  case Nil => "vacía"
  case h :: Nil => s"uno: $h"
  case h :: t => s"cabeza: $h"

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

El pattern matching es la feature más potente de Scala — combina switch/case, destructuring y type checking. Con sealed trait, el compilador verifica la exhaustividad (avisa si falta un caso). Los guards (if n > 10) añaden condiciones arbitrarias. Los patrones de lista (h :: t) descomponen recursivamente — la base de los algoritmos funcionales.

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

// Using (parámetro implícito):
def máximo[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 palabras: List[String] = s.split(" ").toList

"hola mundo".gritar  // "HOLA MUNDO!"
"a b c".palabras     // List("a","b","c")

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

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

given/using (Scala 3) sustituye los implicit de Scala 2 — given define instancias disponibles, using las inyecta automáticamente. Los extension methods añaden métodos a tipos existentes sin wrapper classes. Juntos, implementan el patrón Type Class: comportamiento polimórfico sin herencia. Más claro y seguro que implicit.

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

// Con yield (devuelve colección):
val cuadrados = for i <- 1 to 10 yield i * i

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

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

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

// Equivalente a map/flatMap/filter

Las for comprehensions son azúcar sintáctico para map/flatMap/filter — funcionan con cualquier tipo que implemente esos métodos (List, Option, Future, Either). Con yield, devuelven colección; sin yield, ejecutan side effects. Múltiples generadores producen el producto cartesiano. None/Left hace short-circuit automático.

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

// Try encapsula excepciones:
val resultado = Try("42".toInt)
// Success(42)

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

// Pattern matching:
resultado match
  case Success(n) => println(s"Número: $n")
  case Failure(e) => println(s"Error: $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 computaciones que pueden lanzar excepciones — Success(valor) o Failure(excepción). Es la alternativa funcional a try/catch: soporta map, flatMap, for comprehensions y pattern matching. recover trata errores específicos. Para errores de dominio, prefiere Either (más explícito); Try es ideal para wrapping de APIs Java.

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

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

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

// Range con step y dirección:
(1 until 10 by 2).foreach(println)
(10 to 1 by -1).foreach(println)

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

En programación funcional, while está desaconsejado (requiere var mutable). Prefiere foreach, map o recursión. Los ranges con by permiten pasos personalizados y dirección inversa. break existe pero es raro — normalmente indica que un enfoque funcional (takeWhile, find, recursión) sería más claro y seguro.

Either (errores tipados)
// Either: Left (error) o Right (éxito)
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"Error: $msg")

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

Either representa dos resultados posibles: Left (error) y Right (éxito). Es el preferido para errores de dominio porque documenta el tipo de error en la signature. Las for comprehensions con Either hacen short-circuit en el primer Left. Convención: Right = éxito (right = "correct"). Sustituye excepciones para errores esperados.

Strings e I/O


8 cards
Operaciones de String
val s = "Hola Mundo"

// Transformaciones:
s.toUpperCase       // "HOLA MUNDO"
s.toLowerCase       // "hola mundo"
s.reverse           // "odnuM aloH"
s.trim              // elimina espacios

// Búsqueda:
s.contains("Mundo")  // true
s.startsWith("Hola") // true
s.indexOf("Mundo")   // 5
s.matches("Ho.*")    // true (regex)

// Substring y split:
s.substring(5)       // "Mundo"
s.substring(0, 4)    // "Hola"
"a,b,c".split(",")   // Array("a","b","c")

// Sustitución:
s.replace("Mundo", "Scala")
"aaa".replaceAll("a", "o")  // "ooo"

// Repetición y padding:
"ab" * 3            // "ababab"
"42".padTo(5, '0')  // "42000"

Los Strings en Scala heredan todos los métodos de java.lang.String más extensiones. split retorna Array (usar .toList para List). replaceAll acepta regex, replace es literal. matches verifica regex en el string entero. Los strings son inmutables — todas las operaciones retornan un nuevo string. padTo rellena hasta la longitud mínima.

StringBuilder y performance
// StringBuilder (mutable, eficiente):
val sb = new StringBuilder
for i <- 1 to 1000 do
  sb.append(s"linea $i\n")
val resultado = sb.toString

// vs concatenación (lento - O(n²)):
var s = ""
for i <- 1 to 1000 do
  s += s"linea $i\n"  // NUEVO objeto en cada iter

// mkString (preferir para colecciones):
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")

// Comparación de performance:
// mkString > StringBuilder >> concatenación
// Para < 10 elementos, la concatenación es OK

La concatenación repetida con + crea un nuevo objeto en cada iteración — O(n²). StringBuilder es mutable y eficiente para construcción incremental. mkString es la forma idiomática para colecciones (acepta prefijo, separador, sufijo). Para menos de 10 elementos, la diferencia es despreciable. En hot loops con muchos strings, StringBuilder es esencial.

Interpolación avanzada
// s-interpolator:
val nombre = "Ana"
val edad = 30
s"$nombre tiene $edad años"
s"${nombre.toUpperCase}!"  // expresión

// f-interpolator (formateo):
val pi = 3.14159
f"$pi%.2f"        // "3.14"
f"$edad%05d"      // "00030"
f"$pi%10.3f"      // "     3.142"

// raw (sin escapes):
raw"C:\nueva\carpeta"  // literal

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

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

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

Scala permite interpoladores personalizados vía implicit class en StringContext — librerías como sql, json, uri usan este mecanismo. El f-interpolator soporta todo el formateo printf (%d, %s, %.2f). stripMargin elimina el prefijo | en strings multilínea. Las triple-quotes preservan el formato.

Parsing y conversión
// Parsing seguro (Option):
"42".toIntOption       // Some(42)
"abc".toIntOption      // None
"3.14".toDoubleOption  // Some(3.14)

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

// Parsing con Either:
def parseInt(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"No es 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 (precisión):
val precio = BigDecimal("19.99")
val total = precio * 3  // 59.97 (¡exacto!)

El parsing en Scala debe ser seguro: toIntOption retorna Option (sin excepción), Try captura excepciones, Either documenta errores en el tipo. Los pipelines con for comprehension propagan fallos automáticamente. BigDecimal es obligatorio para valores monetarios — Double tiene errores de precisión (0.1 + 0.2 != 0.3).

Regex en Scala
import scala.util.matching.Regex

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

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

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

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

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

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

Regex en Scala usa scala.util.matching.Regex — creado con .r en cualquier string. El pattern matching con regex extrae grupos directamente (case pattern(d, m, a)). findAllIn retorna un iterator de matches. replaceAllIn sustituye todas las ocurrencias. Las triple-quotes evitan el escaping de backslashes. Se integra naturalmente con pattern matching.

Console y argumentos
// Output:
println("Con newline")
print("Sin newline")
Console.err.println("¡Error!")

// Formatted output:
printf("%s tiene %d años%n", "Ana", 30)
println(f"$pi%.2f")

// Input:
val nombre = scala.io.StdIn.readLine("Nombre: ")
val edad = scala.io.StdIn.readInt("Edad: ")

// 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 y env:
sys.props("java.version")
sys.env.getOrElse("HOME", "/tmp")

println/print para output estándar. Console.err para stderr. scala.io.StdIn para input del usuario (readLine, readInt). @main (Scala 3) con varargs recibe argumentos CLI. sys.props accede a system properties, sys.env a variables de entorno. printf usa formateo Java.

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

// Leer fichero:
val source = Source.fromFile("datos.txt")
val contenido = source.mkString
source.close()

// Leer línea a línea:
val lineas = Source.fromFile("datos.txt")
  .getLines().toList

// Escribir:
val writer = new PrintWriter("salida.txt")
writer.write("Linea 1\n")
writer.println("Linea 2")
writer.close()

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

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

scala.io.Source es la forma clásica de leer ficheros — siempre cerrar con close(). Using (2.13+) cierra automáticamente (try-with-resources). java.nio.file.Files es la API moderna (más performante para ficheros grandes). PrintWriter para escritura. Para paths complejos, usa os-lib (librería de Li Haoyi).

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

// Iterar string (chars):
"Café".foreach(c => println(c.toInt))

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

// codePoints (Unicode real):
"Café".codePointCount(0, "Café".length)  // 4

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

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

Char en Scala es UTF-16 (2 bytes). Métodos como isLetter, isDigit, toUpper/toLower son convenientes. Los ranges de chars ('a' to 'z') generan secuencias. Para Unicode completo (emoji, CJK), usa codePointCount y codePoints. String.length cuenta UTF-16 units, no codepoints.

Patterns e Idioms


8 cards
Pipeline funcional
// Pipeline de transformaciones:
val resultado = datos
  .map(limpiar)
  .filter(validar)
  .map(transformar)
  .foldLeft(resumen)(combinar)

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

// Composición de funciones:
val procesar = trim andThen toLowerCase andThen slugify
val resultado2 = procesar("  Hola Mundo  ")

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

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

El pipeline encadena transformaciones sin estado intermedio — cada paso recibe el resultado del anterior. El pipe operator |> (Scala 3) hace la dirección explícita. andThen compone funciones left-to-right. El patrón tap permite side effects en el pipeline sin interrumpir el flujo. Más legible y testeable que variables temporales.

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

// 2. Instancias 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. Instancia 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"
"hola".toJson        // ""hola""
List(1,2,3).toJson   // "[1,2,3]"

El Type Class pattern tiene 5 pasos: definir la trait, crear instancias given, derivar instancias compuestas, añadir syntax extension, usar. Permite polimorfismo ad-hoc sin herencia — los tipos existentes ganan comportamiento nuevo sin modificación. Es el patrón central en Cats, Circe, http4s. Más flexible que la herencia: una instancia por tipo, sin diamond problem.

Builder inmutable
// Builder con 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 una NUEVA instancia
// Thread-safe, sin mutación

El Builder inmutable usa case class + copy — cada with* retorna una nueva instancia (thread-safe, sin mutación). Los default args hacen todos los campos opcionales. Más seguro que el Builder mutable (GoF) — imposible un estado inconsistente. El patrón es ubicuo en librerías Scala (Akka, http4s, doobie). Fluent API sin side effects.

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

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

// Validaciones independientes:
def validarNombre(s: String): Errors[String] =
  if s.nonEmpty then s.validNel
  else "Nombre vacío".invalidNel

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

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

// Acumular TODOS los errores:
case class User(nombre: String, edad: Int, email: String)

def validarForm(nombre: String, edad: Int, email: String)
  : Errors[User] =
  (validarNombre(nombre), validarEdad(edad), validarEmail(email))
    .mapN(User.apply)

// Either hace short-circuit (solo 1er error)
// Validated acumula todos

Validated (Cats) acumula TODOS los errores de validación — diferente de Either que para en el primero. ValidatedNel = Validated[NonEmptyList[E], A]. mapN combina N validaciones y aplica la función si todas pasan. Ideal para formularios (mostrar todos los errores de una vez). invalidNel/validNel son syntax sugar.

Strategy con funciones
// Strategy = pasar funciones:
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(datos: List[Int], s: Strategy) = s(datos)
ordenar(List(3,1,4,1,5), ascending)
ordenar(List(3,1,4,1,5), descending)

// Comparator como función:
case class User(nombre: String, edad: Int)
val users = List(User("Ana", 30), User("Ray", 25))

users.sortBy(_.edad)
users.sortBy(_.nombre)
users.sortWith((a, b) => a.edad > b.edad)

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

El Strategy pattern en FP es simplemente pasar funciones como parámetros — type alias para claridad. Elimina jerarquías de clases/traits para comportamiento variable. sortBy/sortWith aceptan estrategias de ordenación como funciones. Las higher-order strategies (retry) encapsulan patrones de ejecución. Más conciso y componible 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

// Transición (función 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  // transición inválida

// Ejecutar secuencia:
val events = List(Push, Pull, Lock, Unlock)
val final_state = events.foldLeft(Closed: DoorState)(transition)

Las state machines en FP son funciones puras (State, Event) => State — sin mutación, fácilmente testeables. El pattern matching con la tupla (state, event) define todas las transiciones. foldLeft ejecuta secuencias de eventos. Las transiciones inválidas retornan el estado actual (o Either para logging). Usado en Akka FSM, http4s middleware y UI state.

ADTs y modelado
// Modelar el dominio con 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

// Procesamiento exhaustivo:
def procesar(p: Payment): String = p match
  case Card(num, _) => s"Tarjeta: ${num.takeRight(4)}"
  case BankAccount(iban) => s"IBAN: $iban"
  case Cash => "Efectivo"

// 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

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

Los ADTs (Algebraic Data Types) modelan dominios con variantes finitas — sealed trait + case class/case object. El compilador verifica la exhaustividad (imposible olvidar un caso). El pattern matching con tuplas (state, event) modela state machines. Sustituye jerarquías OOP complejas con código más simple y seguro. Patrón central en Scala.

Idioms y convenciones
// 1. Preferir val sobre var
// 2. Preferir inmutable sobre mutable
// 3. Option en vez de null
// 4. Either en vez de excepciones (dominio)
// 5. Pattern matching en vez de if/else chains

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

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

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

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

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

// sealed + match en vez de visitor pattern

Idioms de Scala: val por defecto, inmutable por defecto, Option en vez de null, Either en vez de excepciones para errores esperados. Named/default args eliminan overloads. Las case classes documentan mejor que las tuples. Los extension methods sustituyen utility classes. Las for comprehensions son más legibles que los flatMap chains. sealed + match sustituye el visitor pattern.