Skip to content

Scala Hoja de referencia

Lenguaje funcional/OO híbrido en la JVM.

01

Fundamentos

Variables y Tipos

Prefiera val (inmutable) sobre var (mutable) para código más seguro y predecible. Scala infiere tipos, pero las anotaciones explícitas ayudan a la legibilidad para APIs públicas. Todo es un objeto—sin primitivos (Int, Boolean son clases).

scala
val name = "Alice"   // immutable (preferred)
var age = 30          // mutable
val pi: Double = 3.14159
val isDev: Boolean = true
val nums: List[Int] = List(1, 2, 3)
println(name.getClass)  // class java.lang.String

Interpolación de Cadenas

s"..." habilita interpolación ${expr}. f"..." añade formato estilo printf (%s, %.2f). raw"..." deshabilita secuencias de escape. Estos prefijos hacen que la construcción de cadenas sea de tipo seguro y legible.

scala
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg")  // formatted
println(raw"No \n escape")  // raw string

Tuplas

Las tuplas agrupan 2-22 valores heterogéneos. Acceda vía _1, _2 (indexación base 1). Deconstruya con val (a, b) = tuple. Para más de 2 elementos, prefiera case classes para campos nombrados y mejor legibilidad.

scala
val pair = ("Alice", 30)
println(pair._1)  // Alice
println(pair._2)  // 30
val (name, age) = pair  // destructure
println(s"${name}: ${age}")
// Scala 3: val p = ("a", 1, 2.0)
val triple = ("a", 1, 2.0)
println(triple._3)  // 2.0

Inferencia de Tipos y Ascripción

Scala infiere tipos para variables locales y tipos de retorno. Use tipos explícitos para APIs públicas, funciones recursivas y casos ambiguos. La ascripción de tipo (expr: Type) fuerza un tipo—útil para upcasting o desambiguación.

scala
val x = 42           // Int inferred
val y: Long = 42      // explicit Long
val z = 42: Long      // type ascription
val list = List(1, 2, 3)  // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2  // return type inferred

Unit y Nothing

Unit es como void (un valor: ()). Nothing es el tipo bottom sin instancias—usado para funciones que nunca retornan (throw, bucle infinito). Nothing es un subtipo de todos los tipos, habilitando inferencia de tipos flexible.

scala
def printIt(x: Int): Unit = println(x)  // like void
val u: Unit = ()    // Unit has one value: ()
// Nothing is the bottom type—no instances
def error(msg: String): Nothing =
  throw new RuntimeException(msg)
// Nothing is a subtype of everything
val n: Nothing = error("boom")
02

Cadenas

Métodos Comunes de Cadena

Las cadenas de Scala son cadenas de Java con métodos extra vía conversiones implícitas (StringOps). La mayoría de los métodos devuelven nuevas cadenas (inmutables). Use estos en lugar de bucles manuales para claridad y corrección.

scala
val s = "Hello, World"
println(s.length)        // 12
println(s.toUpperCase)   // HELLO, WORLD
println(s.toLowerCase)   // hello, world
println(s.split(", "))   // Array(Hello, World)
println(s.replace("o", "0"))  // Hell0, W0rld
println(s.reverse)       // dlroW ,olleH
println(s.contains("World"))  // true

Cadenas Multilínea (Triples Comillas)

Las cadenas con triples comillas preservan todos los espacios en blanco y nuevas líneas. Use stripMargin con | para alinear código limpiamente—solo el texto después de | se mantiene. Ideal para SQL, JSON o plantillas embebidas en código.

scala
val sql = """
  SELECT * FROM users
  WHERE age > 18
  ORDER BY name
"""
println(sql.trim)
// StripMargin for clean indentation
val text = """|Hello
              |World""".stripMargin
println(text)  // Hello
World

Construcción de Cadenas

mkString une colecciones con prefijo/sufijo opcional—idiomático y eficiente. Use StringBuilder para construir cadenas grandes en bucles. Evite concatenación + repetida en bucles (crea muchos objetos intermedios).

scala
val parts = List("apple", "banana", "cherry")
println(parts.mkString(", "))     // apple, banana, cherry
println(parts.mkString("[", ", ", "]"))  // [apple, banana, cherry]
val sb = new StringBuilder
for (p <- parts) sb.append(p).append(" ")
println(sb.toString.trim)

Cadena a Número

toInt/toDouble lanzan en entrada inválida. Use toIntOption (Scala 2.13+) para análisis seguro devolviendo Option. Para análisis masivo, use Try o Either para manejar errores funcionalmente sin excepciones.

scala
val n = "42".toInt        // 42
val d = "3.14".toDouble    // 3.14
val b = "true".toBoolean   // true
val safe = "abc".toIntOption  // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result)  // 0

Regex

.r convierte una cadena a Regex. findFirstIn devuelve Option, findAllIn devuelve un iterador. Use en coincidencia de patrones con case email(e) => para extracción. Regex es Pattern de Java bajo el capó.

scala
import scala.util.matching.Regex
val email: Regex = "[\w.]+@[\w]+\.[a-z]+".r
val text = "Contact: [email protected]"
email.findFirstIn(text) match {
  case Some(e) => println(s"Found: ${e}")
  case None => println("No email")
}
val replaced = "[0-9]+".r.replaceAllIn("a1b2c3", "#")
println(replaced)  // a#b#c#
03

Estructuras de Datos

List y Seq

List es una lista enlazada inmutable—O(1) head/prepend, O(n) acceso aleatorio. Use Vector para acceso aleatorio (O(1) efectivo). +: prepend, :+ append. Prefiera colecciones inmutables para seguridad de subprocesos.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.head)      // 1
println(nums.tail)      // List(2,3,4,5)
println(nums.reverse)   // List(5,4,3,2,1)
println(nums.take(2))   // List(1, 2)
println(nums.drop(2))   // List(3, 4, 5)
println(nums.mkString)  // 12345
val combined = 0 +: nums :+ 6  // List(0,1,2,3,4,5,6)

Map

Map es inmutable—las operaciones devuelven nuevos Maps. Use get(key) para acceso Option, getOrElse para predeterminados. + añade/actualiza, - elimina. Para maps mutables, use scala.collection.mutable.Map. Las claves deben ser Hashable.

scala
val ages = Map("Alice" -> 30, "Bob" -> 25)
println(ages("Alice"))            // 30 (throws if missing)
println(ages.getOrElse("Eve", 0)) // 0 (safe)
val updated = ages + ("Eve" -> 28)  // new Map
val removed = ages - "Bob"
ages.foreach { case (k, v) => println(s"${k}: ${v}") }
println(ages.keys)  // Set(Alice, Bob)

Set

Set es inmutable con O(1) contains. union (|), intersect (&), diff (~) para operaciones de conjuntos. + añade, - elimina. Use para desduplicación y prueba de pertenencia. Variante mutable: scala.collection.mutable.Set.

scala
val a = Set(1, 2, 3)
val b = Set(3, 4, 5)
println(a union b)        // Set(1,2,3,4,5)
println(a intersect b)    // Set(3)
println(a diff b)         // Set(1,2)
println(a subsetOf(Set(1,2,3,4)))  // true
val added = a + 6  // Set(1,2,3,6)

Option (Seguridad de Nulos)

Option reemplaza null—Some(value) o None. Use map/filter/flatMap para transformaciones, getOrElse para predeterminados. Las for-comprehensions funcionan en Option. Esto elimina NullPointerException haciendo explícita la ausencia en el tipo.

scala
def findUser(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
val name = findUser(1)
println(name.getOrElse("Unknown"))  // Alice
println(name.map(_.toUpperCase))    // Some(ALICE)
println(name.filter(_.startsWith("A")))  // Some(Alice)
val result = for {
  n <- findUser(1)
  if n.startsWith("A")
} yield n.toUpperCase  // Some(ALICE)

Array y Vector

Array es un array de Java mutable (el más rápido, pero sin actualizaciones funcionales). Vector es inmutable con acceso aleatorio y actualizaciones efectivamente O(1)—preferido para colecciones inmutables de acceso aleatorio. Use List para secuencial, Vector para indexado.

scala
val arr = Array(1, 2, 3, 4)  // mutable, Java array
arr(0) = 10
println(arr(0))  // 10
val vec = Vector(1, 2, 3, 4)  // immutable, fast random access
println(vec(2))  // 3
val updated = vec.updated(0, 10)  // Vector(10,2,3,4)
// Vector: O(1) random access + immutable
// Array: mutable, Java interop, fastest
04

Flujo de Control

If / Else (Expresión)

En Scala, if/else es una expresión que devuelve un valor. Esto elimina la necesidad de un operador ternario. Ambas ramas deben tener tipos compatibles. Use esto para asignación condicional concisa.

scala
val score = 85
val grade =
  if (score >= 90) "A"
  else if (score >= 80) "B"
  else if (score >= 70) "C"
  else "F"
println(grade)  // B
// if returns a value—no ternary operator needed

For Comprehension

Las for comprehensions iteran y pueden filtrar (guards if) y transformar (yield). Sin yield, es un bucle; con yield, construye una colección. to incluye el final, until lo excluye. Equivalente a cadenas flatMap/map/filter.

scala
for (i <- 1 to 5) println(i)  // 1 2 3 4 5
for (i <- 1 until 5) print(i)  // 1 2 3 4
// With yield (generates collection)
val doubled = for (n <- List(1,2,3)) yield n * 2
println(doubled)  // List(2, 4, 6)
// With filter (guard)
val evens = for (n <- 1 to 10 if n % 2 == 0) yield n
println(evens.toList)  // List(2,4,6,8,10)

Match (Coincidencia de Patrones)

match es la potente coincidencia de patrones de Scala—como switch con esteroides. Admite literales, OR (|), guards (if), coincidencia de tipo y deconstrucción. Debe ser exhaustivo (el compilador advierte en casos faltantes). Es una expresión que devuelve un valor.

scala
val n = 2
val label = n match {
  case 0 => "zero"
  case 1 | 2 | 3 => "small"
  case x if x < 10 => "medium"
  case _ => "large"
}
println(label)  // small
// Match on types
def describe(x: Any): String = x match {
  case i: Int => s"Int: ${i}"
  case s: String => s"String: ${s}"
  case _ => "unknown"
}

While y Do-While

while/do-while son bucles imperativos que devuelven Unit. Requieren estado mutable (var). Prefiera for-comprehensions o recursión para Scala funcional idiomático. Use while solo cuando el rendimiento lo exija o para efectos secundarios.

scala
var i = 0
while (i < 3) {
  println(i)
  i += 1
}
var j = 0
do {
  println(j)
  j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutability

Try / Catch / Finally

try/catch/finally es como Java pero usa coincidencia de patrones en catch. Prefiera Try para manejo de errores funcional—envuelve excepciones como valores Success/Failure, habilitando cadenas map/flatMap sin código repetitivo try/catch.

scala
import scala.util.{Try, Success, Failure}
val result = try {
  "abc".toInt
} catch {
  case e: NumberFormatException => 0
} finally {
  println("cleanup")
}
println(result)  // 0
// Functional alternative
val r2 = Try("abc".toInt).getOrElse(0)
println(r2)  // 0
05

Funciones

Definición de Métodos

Los métodos usan def name(params): ReturnType = body. Se admiten parámetros predeterminados y argumentos nombrados. Retorno Unit = solo efecto secundario. Los cuerpos de expresión única omiten llaves. = es requerido (sin él, es un procedimiento que devuelve Unit).

scala
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
  s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4))           // 7
println(greet("Alice"))      // Hello, Alice!
println(greet("Bob", greeting = "Hi"))  // named arg

Lambda (Función Anónima)

Lambdas: (params) => body. Use _ como abreviatura para parámetros únicos (x => x * 2 se convierte en _ * 2). Múltiples _ se refieren a diferentes parámetros (_ + _). Las lambdas son de primera clase—páselas a map, filter, reduce, etc.

scala
val square = (x: Int) => x * x
println(square(5))  // 25
val nums = List(1, 2, 3)
println(nums.map(_ * 2))      // List(2, 4, 6)
println(nums.filter(_ > 1))   // List(2, 3)
println(nums.reduce(_ + _))   // 6
// _ is shorthand for the parameter

Funciones de Orden Superior

Las funciones de orden superior toman o devuelven funciones. Esto habilita abstracciones potentes: map/filter/reduce, composición, aplicación parcial. makeAdder devuelve un closure capturando n. Este es el corazón de la programación funcional.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5))  // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10))  // 15
// Functions returning functions = currying-like

Currying y Aplicación Parcial

El currying divide parámetros en múltiples listas: def f(a)(b). Aplique parcialmente con _ para crear funciones especializadas. Múltiples listas de parámetros mejoran la inferencia de tipos (el compilador puede inferir los tipos de f desde la lista). Común en APIs de colección.

scala
def add(a: Int)(b: Int): Int = a + b  // curried
val add5 = add(5)_  // partially applied
println(add5(3))  // 8
// Multiple parameter lists
def foldLeft[A, B](list: List[A])(z: B)(f: (B, A) => B): B = ???
// Helps type inference
val sum = List(1,2,3).foldLeft(0)(_ + _)

By-Name y Lazy

Los parámetros by-name (=> T) se evalúan perezosamente en cada uso—útiles para logging (saltar msg costoso si deshabilitado) y estructuras de control personalizadas. lazy val difiere la inicialización hasta el primer acceso—use para valores costosos u opcionales.

scala
// By-name parameter: evaluated on each use
def debug(msg: => String): Unit =
  if (debugEnabled) println(msg)
// Lazy evaluation
lazy val expensive = computeHeavy()
println(expensive)  // computed now
def computeHeavy(): Int = { println("computing"); 42 }
06

Clases y POO

Clase y Constructor

Los parámetros del constructor primario están en la firma de la clase. Los parámetros val se vuelven campos inmutables (getter público), var mutables. El cuerpo de la clase ES el constructor. Los constructores auxiliares usan def this(...) y deben llamar a otro constructor.

scala
class Person(val name: String, val age: Int) {
  // Constructor params with val/var become fields
  def greet: String = s"Hi, I'm ${name}"
  def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet)   // Hi, I'm Alice
println(p.name)    // Alice (val field)
println(p.isAdult) // true

Case Class

Las case classes son clases de datos inmutables con equals, hashCode, toString, copy y companion object con apply/unapply auto-generados. Use para modelado de datos y coincidencia de patrones. Sin 'new' necesario. Son la base de los ADTs en Scala.

scala
case class Point(x: Int, y: Int)
val p1 = Point(3, 4)  // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2)  // true (value equality)
val moved = p1.copy(x = 5)  // Point(5, 4)
println(p1.x, p1.y)  // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companion

Traits (Interfaces con Implementación)

Los traits son como interfaces de Java pero pueden tener implementación. Una clase puede mezclar múltiples traits (con extends/with). Los traits habilitan herencia múltiple de comportamiento. Use para interfaces compartidas, mixins y modificaciones apilables vía linearización.

scala
trait Greetable {
  def name: String  // abstract
  def greet: String = s"Hello, ${name}"  // concrete
}
trait Named {
  val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet)  // Hello, Alice
// Stackable traits via linearization

Object (Singleton)

object declara un singleton (una instancia). Los companion objects (mismo nombre que una clase) contienen métodos tipo static, factories (apply) y extractores (unapply). Use apply para métodos factory así los llamadores omiten 'new'. Esto es Scala idiomático.

scala
object Config {
  val version = "1.0"
  def load(): Map[String, String] = Map("key" -> "value")
}
println(Config.version)  // 1.0
// Companion object (same name as class)
class Person(val name: String)
object Person {
  def apply(name: String): Person = new Person(name)
}
val p = Person("Alice")  // uses apply, no 'new'

Herencia y Clase Abstracta

abstract class puede tener miembros no implementados. Use extends para heredar, override para redefinir. Prefiera traits para mixins (herencia múltiple). Use abstract class cuando necesite parámetros de constructor o quiera un tipo base. Herencia simple de clases.

scala
abstract class Shape {
  def area: Double  // abstract
  def describe: String = s"Area: ${area}"
}
class Circle(r: Double) extends Shape {
  def area: Double = math.Pi * r * r
}
val c = new Circle(5)
println(c.describe)  // Area: 78.53...
// override required for concrete members
// abstract class vs trait: use abstract for base, trait for mixins
07

Colecciones y Funcional

Map / Filter / Fold

map transforma, filter selecciona, reduce/foldLeft agregan. Estos son el núcleo de la transformación funcional de datos. foldLeft toma una semilla y es asociativo; reduce requiere no vacío. Use estos en lugar de bucles para claridad e inmutabilidad.

scala
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2))           // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0))   // List(2,4)
println(nums.reduce(_ + _))        // 15
println(nums.foldLeft(0)(_ + _))   // 15
println(nums.sum)                  // 15
println(nums.mkString(", "))       // 1, 2, 3, 4, 5

FlatMap y For-Comprehension

flatMap mapea y aplana en un paso—esencial para colecciones anidadas y operaciones monádicas. Las for-comprehensions son azúcar sintáctico para cadenas flatMap/map/filter. Use para transformaciones anidadas complejas—es más legible.

scala
val nested = List(List(1, 2), List(3, 4))
println(nested.flatten)        // List(1,2,3,4)
println(nested.flatMap(_.map(_ * 2)))  // List(2,4,6,8)
// Equivalent for-comprehension:
val result = for {
  inner <- nested
  n <- inner
} yield n * 2
println(result)  // List(2,4,6,8)

Agrupación y Ordenamiento

groupBy particiona por una clave en un Map. sorted ordena naturalmente, sortBy por una función clave, sortWith con un comparador. Estos devuelven nuevas colecciones (inmutables). Use para análisis de datos, categorización y ordenamiento.

scala
val words = List("apple", "bat", "cat", "ant")
val byFirst = words.groupBy(_.head)
// Map(a -> List(apple, ant), b -> List(bat), c -> List(cat))
println(byFirst)
val sorted = words.sorted  // List(ant, apple, bat, cat)
val byLen = words.sortBy(_.length)  // List(ant, bat, cat, apple)
val desc = words.sortWith(_ > _)  // descending
println(sorted, byLen)

Either y Try

Try envuelve excepciones como valores Success/Failure. Either representa Left (error) o Right (éxito)—use para errores de dominio donde quiera distinguir tipos de error. Ambos admiten map/flatMap para propagación funcional de errores.

scala
import scala.util.{Try, Success, Failure}
def parse(s: String): Try[Int] = Try(s.toInt)
parse("42") match {
  case Success(n) => println(s"OK: ${n}")
  case Failure(e) => println(s"Err: ${e.getMessage}")
}
// Either for domain errors
def divide(a: Int, b: Int): Either[String, Int] =
  if (b == 0) Left("div by zero") else Right(a / b)
println(divide(10, 2))  // Right(5)
println(divide(10, 0))  // Left(div by zero)

Colecciones Perezosas (View y Lazy)

.view hace las colecciones perezosas—las operaciones se difieren hasta forzar (toList, sum, etc.). Evita colecciones intermedias para mejor rendimiento en datos grandes. LazyList (anteriormente Stream) habilita secuencias infinitas—elementos calculados bajo demanda.

scala
val nums = (1 to 1000000).view
val result = nums
  .filter(_ % 2 == 0)
  .map(_ * 2)
  .take(5)
  .toList  // forces evaluation
println(result)  // List(4, 8, 12, 16, 20)
// view = lazy, no intermediate collections
// LazyList (Stream) for infinite sequences
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
08

Coincidencia de Patrones

Coincidencia de Case Classes

Sealed traits + case classes forman Tipos de Datos Algebraicos (ADTs). El compilador verifica exhaustividad—añada un caso y advierte dónde las coincidencias necesitan actualización. La coincidencia de patrones deconstruye case classes directamente. Esto es modelado idiomático de Scala.

scala
sealed trait Shape
case class Circle(r: Double) extends Shape
case class Square(s: Double) extends Shape
case class Rect(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
  case Circle(r) => math.Pi * r * r
  case Square(s) => s * s
  case Rect(w, h) => w * h
}
println(area(Circle(5)))  // 78.53...

Guards y Condiciones

Los guards (if condition) añaden verificaciones en tiempo de ejecución a los patrones. Hacen la coincidencia más expresiva. El orden importa—primera coincidencia gana. Use guards para rangos, condiciones o lógica compleja que los patrones simples no pueden expresar.

scala
val n = 15
val desc = n match {
  case x if x < 0 => "negative"
  case 0 => "zero"
  case x if x % 2 == 0 => "even"
  case _ => "odd"
}
println(desc)  // odd
// Guards add boolean conditions to patterns

Coincidencia de Colecciones

El patrón cons :: deconstruye listas en head y tail. _* coincide cero o más elementos en arrays/listas. Estos patrones habilitan procesamiento recursivo de listas y descomposición estructural. Potente para análisis y traversal de árboles.

scala
val list = List(1, 2, 3, 4)
list match {
  case Nil => println("empty")
  case head :: Nil => println(s"one: ${head}")
  case head :: tail => println(s"head=${head}, rest=${tail}")
  case _ => println("other")
}
// head :: tail destructures a list
val arr = Array(1, 2, 3)
arr match {
  case Array(1, _*) => println("starts with 1")
  case _ => println("other")
}

Coincidencia de Option y Either

La coincidencia de patrones en Option/Either es idiomática—Some(x)/None, Right(x)/Left(e). Las for-comprehensions se desugarean a flatMap con match. Esto hace que el flujo de manejo de errores sea natural sin verificaciones if-else explícitas.

scala
def find(id: Int): Option[String] =
  if (id == 1) Some("Alice") else None
find(1) match {
  case Some(name) => println(s"Found: ${name}")
  case None => println("Not found")
}
// In for-comprehensions
val result = for {
  name <- find(1)
  upper = name.toUpperCase
} yield upper
println(result)  // Some(ALICE)

Extractores (unapply)

Los extractores personalizados vía unapply habilitan coincidencia de patrones en cualquier tipo. El método unapply devuelve Option de los valores extraídos. Esto le permite definir sus propios patrones—potente para DSLs y análisis. Las case classes auto-generan unapply.

scala
object Email {
  def unapply(s: String): Option[(String, String)] = {
    val parts = s.split("@")
    if (parts.length == 2) Some((parts(0), parts(1))) else None
  }
}
"[email protected]" match {
  case Email(user, domain) =>
    println(s"User: ${user}, Domain: ${domain}")
  case _ => println("Not an email")
}
09

Genéricos e Implicits

Clases y Métodos Genéricos

Los genéricos (parámetros de tipo [A]) escriben código de tipo seguro y reutilizable. Use [A] para un solo tipo, [A, B] para dos. La inferencia de tipos usualmente averigua el tipo. Los genéricos se borran en tiempo de ejecución (limitación JVM) pero se verifican en tiempo de compilación.

scala
class Stack[A] {
  private var items: List[A] = Nil
  def push(x: A): Unit = { items = x :: items }
  def pop: Option[A] = items.headOption
}
val s = new Stack[Int]
s.push(1); s.push(2)
println(s.pop)  // Some(2)
def first[A](list: List[A]): Option[A] = list.headOption
println(first(List("a", "b")))  // Some(a)

Cotas de Tipo

<: cota superior (A es un subtipo), >: cota inferior (A es un supertipo). Las cotas de contexto (A: Ordering) requieren un valor implícito de ese tipo. Las cotas de vista (A <% B) están deprecadas—use cotas de contexto en su lugar. Estas restringen parámetros de tipo.

scala
// Upper bound: A must be Animal or subclass
class Box[A <: Animal](val content: A)
// Lower bound: A must be Dog or superclass
class Kennel[A >: Dog](val occupant: A)
// Context bound: A must have an Ordering
def max[A: Ordering](a: A, b: A): A =
  if (implicitly[Ordering[A]].gt(a, b)) a else b
abstract class Animal { def name: String }
class Dog extends Animal { def name = "Rex" }

Parámetros Implícitos

Los parámetros implícitos son inyectados por el compilador desde el ámbito. Use para configuración, type classes o dependencias que no quiera pasar por todas partes. Declare con implicit val/def. El compilador busca en el ámbito envolvente y companion objects.

scala
def greet(name: String)(implicit greeting: String): String =
  s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice"))  // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi"))  // Hi, Bob! (explicit override)
// Compiler finds implicit in scope

Type Classes (Conversiones Implícitas)

Las type classes (vía implicits) añaden comportamiento a tipos sin modificarlos—polimorfismo ad-hoc. Defina un trait, proporcione instancias implícitas y use parámetros implícitos. Así es como funcionan Ordering, Numeric y Show. Más flexible que la herencia.

scala
trait Show[A] { def show(a: A): String }
object Show {
  implicit val intShow: Show[Int] = (a: Int) => a.toString
  implicit val strShow: Show[String] = identity
}
def printIt[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))
printIt(42)       // 42
printIt("hello")  // hello
// Type class: ad-hoc polymorphism

Métodos de Extensión (Scala 2)

Las implicit classes añaden métodos de extensión a tipos existentes. Defina implicit class con un solo parámetro, y sus métodos se vuelven disponibles en ese tipo. Use para añadir métodos de utilidad a Int, String, etc. Scala 3 usa la sintaxis 'extension' más limpia.

scala
implicit class IntOps(val n: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to n).foreach(_ => f)
  def squared: Int = n * n
}
5.times { print("hi") }  // hihihihihi
println(5.squared)  // 25
// Scala 3: extension (n: Int) def squared = n * n
10

Concurrencia y Future

Future y Async

Future representa una computación asíncrona. onComplete registra un callback. Nunca bloquee (Await.result) en servidores web de producción—ocupa subprocesos. Use for-comprehensions para encadenar futures funcionalmente. Requiere un ExecutionContext.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
  Thread.sleep(1000)
  42
}
f.onComplete {
  case scala.util.Success(v) => println(s"Got ${v}")
  case scala.util.Failure(e) => println(s"Err: ${e}")
}
// Don't block in production—use callbacks or for-comprehensions

Composición de Futures

Las for-comprehensions en Futures las ejecutan secuencialmente (cada una espera la anterior). Para ejecución paralela, inicie todos los Futures primero, luego use Future.sequence para combinar. Future.traverse mapea + secuencia en un paso. Esta es la forma idiomática de componer trabajo async.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f1 = Future { 10 }
val f2 = Future { 20 }
val sum = for {
  a <- f1
  b <- f2
} yield a + b  // Future(30)
// Parallel execution
val results = Future.sequence(List(
  Future { 1 }, Future { 2 }, Future { 3 }
))
results.map(_.sum)  // Future(6)

Colecciones Paralelas

.par convierte una colección a una versión paralela—las operaciones usan múltiples subprocesos automáticamente. Bueno para trabajo limitado por CPU en colecciones grandes. Tenga cuidado: las operaciones no asociativas (como resta) pueden dar resultados diferentes. No para trabajo limitado por E/S.

scala
val nums = (1 to 1000000).toList
val sum = nums.par.sum  // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collections

Promise (Future Manual)

Promise es el lado escribible de un Future—lo completa manualmente con success/failure. Use cuando tienda un puente de APIs basados en callback a Futures, o cuando necesite completar un Future desde múltiples lugares. Future es de solo lectura; Promise es write-once.

scala
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println)  // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read side

Sync vs Async (Await)

Await.result bloquea el subproceso actual hasta que el Future completa (con timeout). Use solo en pruebas o métodos main—bloquear en código async derrota el propósito. En producción, use callbacks (onComplete, map) o for-comprehensions para permanecer no bloqueante.

scala
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result)  // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks instead
11

Implicits en Profundidad

Parámetros Implícitos

Los parámetros implícitos se pasan automáticamente por el compilador cuando un valor implícito del tipo coincidente está en el ámbito. Esto reduce código repetitivo para parámetros de 'contexto' (ExecutionContext, logging, configuración). El compilador busca: ámbito local, companion objects, ámbito implícito. Siempre puede pasar explícitamente para sobrescribir. El uso excesivo hace el código difícil de rastrear—use para dependencias genuinamente contextuales.

scala
import scala.concurrent.ExecutionContext

// Method with implicit parameter
def process[A](data: List[A])
    (implicit ec: ExecutionContext): Future[Unit] = {
  Future { data.foreach(println) }
}

// The compiler finds an implicit ExecutionContext in scope
implicit val ec: ExecutionContext = ExecutionContext.global
process(List(1, 2, 3))  // ec passed automatically

// Explicitly providing (overrides implicit)
process(List(1, 2, 3))(myCustomEC)

// Multiple implicit parameters
def log(msg: String)(implicit
    level: Level, logger: Logger): Unit = {
  logger.log(level, msg)
}

Conversiones Implícitas

Las conversiones implícitas convierten automáticamente entre tipos cuando se necesita. implicit class (extendiendo AnyVal para cero sobrecarga) añade métodos de extensión a tipos existentes—así es como Scala añade métodos a Int, String, etc. Tenga cuidado: las conversiones implícitas pueden hacer el código confuso (¿qué se está convirtiendo?). Prefiera implicit classes para extensiones sobre implicit defs crudos. Habilite con import scala.language.implicitConversions.

scala
import scala.language.implicitConversions

// Implicit conversion: one type to another
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // intToString(42) called implicitly

// Extension via implicit class (Scala 2.10+)
implicit class RichInt(val self: Int) extends AnyVal {
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
  def squared: Int = self * self
}

5.times { println("hi") }  // prints hi 5 times
3.squared  // 9

// Implicit conversion for type compatibility
implicit def javaToScalaList(jl: java.util.List[Int]): List[Int] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

val javaList: java.util.List[Int] = ???
val scalaList: List[Int] = javaList  // converted

Prioridad de Resolución Implícita

El compilador resuelve implicits por prioridad: ámbito local > companion objects > importados > heredados. Si dos implicits del mismo tipo están igualmente en ámbito, obtiene un error 'ambiguous implicit'. El patrón de trait LowPriorityImplicits proporciona predeterminados que pueden ser sobrescritos por implicits más específicos. Entender el orden de resolución es crucial para diseño de bibliotecas—coloque predeterminados en traits de baja prioridad para que los usuarios puedan sobrescribir.

scala
// Priority of implicit resolution (highest to lowest):
// 1. Local implicit (defined in current scope)
implicit val ec1: ExecutionContext = ec1

// 2. Implicit in companion object
object MyService {
  implicit val ec2: ExecutionContext = ec2  // lower priority
}

// 3. Implicit scope (imported)
import somePackage.Implicits._

// 4. Implicit parameter default (inherited trait)
trait DefaultEc {
  implicit val ec: ExecutionContext = ExecutionContext.global
}

// More specific type wins
implicit def ord1: Ordering[Int] = ???
implicit def ord2: Ordering[Int] = ???  // ambiguous error!

// LowPriorityImplicits trait pattern
object MyLib {
  implicit val high: Ordering[Int] = ???
}
object MyLib extends LowPriorityImplicits
trait LowPriorityImplicits {
  implicit val low: Ordering[Int] = ???  // fallback
}

Cotas de Contexto y Evidence

Las cotas de contexto [A: TypeClass] son azúcar sintáctico para parámetros implícitos—afirman que existe un TypeClass[A] implícito. Use implicitly[TypeClass[A]] (Scala 2) o summon[TypeClass[A]] (Scala 3) para recuperarlo. Las cotas de contexto hacen las restricciones de type class legibles: def sort[A: Ordering]. Múltiples cotas se apilan: [A: Ordering: Numeric]. Esta es la forma idiomática de expresar requisitos de type class.

scala
// Context bound: [A: Ordering] means there's an implicit Ordering[A]
def max[A: Ordering](a: A, b: A): A = {
  val ord = implicitly[Ordering[A]]  // retrieve the implicit
  if (ord.gt(a, b)) a else b
}

// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
  if (ord.gt(a, b)) a else b

// summon (Scala 3) instead of implicitly
def max3[A: Ordering](a: A, b: A): A = {
  val ord = summon[Ordering[A]]
  if (ord.gt(a, b)) a else b
}

// Type class evidence
def sort[A: Ordering](list: List[A]): List[A] =
  list.sorted  // uses the implicit Ordering

// Multiple context bounds
def process[A: Ordering: Numeric](x: A, y: A): A = ???

Ámbito Implícito y Companion Objects

El ámbito implícito es más amplio que solo el ámbito actual—incluye companion objects de los tipos involucrados. Esto es por lo que no necesita importar Ordering[Int]: vive en el companion de Int. Este mecanismo hace las type classes ergonómicas: defina la instancia en el companion del tipo, y está automáticamente disponible. Los package objects contienen implicits compartidos para un paquete. Este diseño habilita uso de type class 'sin import'.

scala
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
  implicit val ordering: Ordering[UserId] =
    Ordering.by(_.value)
}

// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion

// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters

// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }

// Package object for shared implicits
package object myapp {
  implicit val ec: ExecutionContext = ExecutionContext.global
  type Id = Long
}
12

Type Classes

Definir una Type Class

Una type class es un trait parametrizado por tipo, con instancias que proporcionan comportamiento para tipos específicos. A diferencia de la herencia, puede añadir instancias de type class retroactivamente (para tipos que no posee). Show[A] define cómo mostrar A. Las instancias viven en el companion object (ámbito implícito automático). Esto es polimorfismo ad-hoc—diferente comportamiento por tipo sin modificar los tipos. Las type classes son la abstracción más potente de Scala.

scala
// Type class: a trait parameterized by type
trait Show[A] {
  def show(a: A): String
}

// Instances for specific types
object Show {
  // Instance for Int
  implicit val showInt: Show[Int] = (a: Int) => a.toString

  // Instance for String
  implicit val showString: Show[String] = (s: String) => s""$s""

  // Instance for List (recursive)
  implicit def showList[A](implicit s: Show[A]): Show[List[A]] =
    (list: List[A]) => list.map(s.show).mkString("[", ", ", "]")
}

// Usage with implicit parameter
def print[A](a: A)(implicit s: Show[A]): Unit =
  println(s.show(a))

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

Usar Type Classes (azúcar sintáctico)

Las cotas de contexto [A: Show] + summon recuperan instancias de type class. Los métodos de extensión (implicit class) añaden métodos como .show que usan la type class. Esta combinación da una API limpia: 42.show funciona si Show[Int] existe. La biblioteca estándar proporciona muchas type classes: Numeric, Ordering, Eq, Monoid (Cats). Importar sintaxis (Numeric.Implicits._) añade operadores como + y sum que usan la type class.

scala
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
  items.foreach(a => println(summon[Show[A]].show(a)))

// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
  def show(implicit s: Show[A]): String = s.show(a)
}

42.show           // "42"
"hi".show         // ""hi""
List(1,2).show    // "[1, 2]"

// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show

// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
  summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
    summon[Numeric[A]].zero)(summon[Numeric[A]].plus))

// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sum

Type Classes Comunes (Cats/Scalaz)

Cats y Scalaz proporcionan type classes estándar. Monoid (empty + combine) habilita agregación genérica. Functor (map) y Monad (pure + flatMap) abstraen sobre contenedores (List, Option, Future, IO). Eq proporciona igualdad de tipo seguro (sin comparaciones accidentales entre tipos). Estas se componen: una Monad es un Functor, un Monoid es un Semigroup. Las type classes habilitan escribir código genérico y reutilizable que funciona entre muchos tipos.

scala
// Monoid: combine values with empty
trait Monoid[A] {
  def empty: A
  def combine(a: A, b: A): A
}
object Monoid {
  implicit val intAdd: Monoid[Int] = new Monoid[Int] {
    def empty = 0
    def combine(a: Int, b: Int) = a + b
  }
  implicit def listMonoid[A]: Monoid[List[A]] = new Monoid[List[A]] {
    def empty = Nil
    def combine(a: List[A], b: List[A]) = a ++ b
  }
}

// Functor: map over structure
trait Functor[F[_]] {
  def map[A, B](fa: F[A])(f: A => B): F[B]
}

// Monad: chain operations
trait Monad[F[_]] {
  def pure[A](a: A): F[A]
  def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
}

// Eq: type-safe equality
trait Eq[A] {
  def eqv(a: A, b: A): Boolean
}

// Semigroup: combine (no empty)
trait Semigroup[A] {
  def combine(a: A, b: A): A
}

Leyes y Testing de Type Classes

Las leyes de type class son propiedades matemáticas que las instancias deben satisfacer. Monoid requiere asociatividad e identidad. Functor requiere preservación de identidad y composición. Bibliotecas como Cats proporcionan definiciones de leyes; discipline + ScalaCheck las auto-prueba. Las leyes son por qué las type classes son potentes: código genérico (como foldMap) funciona correctamente para cualquier instancia que cumpla las leyes. Siempre verifique que sus instancias satisfacen las leyes—bugs en instancias rompen todo código genérico que las use.

scala
// Type class laws: properties that must hold
// Monoid laws:
// 1. Left identity:  combine(empty, a) == a
// 2. Right identity: combine(a, empty) == a
// 3. Associativity:  combine(a, combine(b, c)) == combine(combine(a, b), c)

// Functor laws:
// 1. Identity:    map(fa)(identity) == fa
// 2. Composition: map(fa)(f andThen g) == map(map(fa)(f))(g)

// Testing laws with ScalaCheck (discipline)
import org.scalacheck.Prop.forAll
import cats.kernel.laws.MonoidLaws

class MonoidSpec extends munit.FunSuite with Discipline {
  checkAll("Int Monoid", MonoidLaws[Int].monoid)
}

// Custom law test
def monoidLeftIdentity[A](implicit m: Monoid[A], arb: Arbitrary[A]) =
  forAll { (a: A) =>
    m.combine(m.empty, a) == a
  }

// Laws make type classes trustworthy:
// if an instance satisfies laws, generic code works correctly

Derivación de Type Class (Scala 3)

Scala 3 simplifica la derivación de type class con la palabra clave 'derives' y Mirror. El compilador puede auto-generar instancias para case classes y enums componiendo instancias de elementos. Esto elimina el código repetitivo de escribir instancias para cada case class (común en Scala 2 con shapeless). Bibliotecas como Cats y Circe admiten derivación de Scala 3. El tipo Mirror da acceso en tiempo de compilación a la estructura de un tipo (tipos de campo, etiquetas) para programación genérica.

scala
// Scala 3: derive type class instances automatically
import scala.deriving.Mirror

trait Show[A] {
  def show(a: A): String
}

object Show {
  // Inline given for derivation
  given showInt: Show[Int] with
    def show(a: Int) = a.toString

  given showString: Show[String] with
    def show(s: String) = s""$s""

  // Derive for products (case classes)
  given showProduct[A](using m: Mirror.ProductOf[A])
      (using ev: Show[m.MirroredElemTypes]): Show[A] with
    def show(a: A): String = ???

  // Or use Scala 3's derivation
  inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}

// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automatically
13

For Comprehensions en Profundidad

For Comprehensions Básicas

Las for comprehensions son azúcar sintáctico para flatMap/map/withFilter. Cada <- es flatMap (excepto el último, que es map). Los guards if se vuelven withFilter. yield hace que devuelva una colección; omitir yield lo hace imperativo (foreach). Esto funciona en cualquier tipo con flatMap/map (Monad): List, Option, Future, Try, IO. Dominar las for-comprehensions es clave para Scala idiomático—reemplazan maps/flatMaps anidados con sintaxis secuencial legible.

scala
// For comprehension: syntactic sugar for flatMap/map
val result = for {
  x <- List(1, 2, 3)
  y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)

// Desugared:
List(1, 2, 3).flatMap { x =>
  List(10, 20).map { y => x + y }
}

// With filters (if guards)
val evens = for {
  x <- 1 to 10
  if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)

// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)

// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x)  // 1, 2, 3

For con Option y Future

Las for-comprehensions funcionan en cualquier Monad. Con Option, cortocircuitan en None (devuelven None). Con Future, cortocircuitan en fallo. Esto hace que el código secuencial async/puede-fallar se lea como código imperativo de línea recta mientras permanece funcional. Cada línea <- puede depender de vinculaciones anteriores. Esto es mucho más limpio que llamadas flatMap anidadas. La misma sintaxis funciona para Try, Either, IO y monads personalizados.

scala
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???

val email: Option[String] = for {
  user <- getUser(42)
  email <- getEmail(user)
} yield email

// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))

// Future: chain async operations
val result: Future[Int] = for {
  user <- fetchUser(1)      // Future[User]
  posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size

// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic composition

For con Either y Manejo de Errores

Either es el manejo de errores tipado de Scala. Las for-comprehensions encadenan Eithers, cortocircuitando en Left (error). Esto es manejo de errores funcional—sin excepciones, los errores son valores. El tipo Left es el error (usualmente String o un sealed trait). Either es right-biased en Scala 2.12+ (map/flatMap operan en Right). Este patrón reemplaza try/catch con propagación de errores componible y de tipo seguro. Validated de Cats es una alternativa para acumular errores.

scala
// Either for error handling
def parseAge(s: String): Either[String, Int] =
  s.toIntOption.toRight(s"not a number: $s")

def validateAge(age: Int): Either[String, Int] =
  if (age >= 0) Right(age) else Left(s"negative: $age")

val result: Either[String, Int] = for {
  age <- parseAge("30")     // Right(30)
  valid <- validateAge(age) // Right(30)
} yield valid

// With error in chain
val error: Either[String, Int] = for {
  age <- parseAge("abc")    // Left("not a number: abc")
  valid <- validateAge(age) // skipped!
} yield valid
// error == Left("not a number: abc")

// Scala 3: for-comprehensions work with Either directly
// (Scala 2 needed either.map(_.right) or withFilter)

Desugareado y Monads Personalizados

Cualquier tipo con flatMap y map admite for-comprehensions—este es el patrón Monad. Defina estos métodos en su tipo para habilitar la sintaxis for. El = (no <-) crea una vinculación local (no desugareado a flatMap). Entender el desugareado ayuda a depurar comprehensions complejas e implementar monads personalizados. El compilador traduce for-comprehensions a cadenas flatMap/map/withFilter. Esto es por lo que for funciona uniformemente entre List, Option, Future, IO, etc.

scala
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions

case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
  def flatMap[B](f: A => Box[B]): Box[B] = f(value)
  def withFilter(p: A => Boolean): Box[A] =
    if (p(value)) this else throw new NoSuchElementException
}

val result = for {
  x <- Box(10)
  y <- Box(20)
  if x < y
} yield x + y
// Box(30)

// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2.   Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
//   Box(20).withFilter(y => x < y).map(y => x + y))

// Assignment within for (= instead of <-)
for {
  x <- List(1, 2, 3)
  doubled = x * 2  // local val
} yield doubled

For vs Map/FlatMap (cuándo usar)

Use for-comprehensions para 2+ operaciones dependientes—son más legibles que flatMaps anidados. Para una sola transformación, map es más claro. Para aplanar, flatMap directamente. Para efectos secundarios (sin resultado), use for sin yield. Las for-comprehencias brillan cuando cada paso depende del anterior (encadenamiento monádico). Hacen que el código async/manejo-de-errores se lea secuencialmente. Evite fors profundamente anidados (>5 niveles)—extraiga helpers para legibilidad.

scala
// For: sequential, dependent operations
val result = for {
  user <- fetchUser(id)
  profile <- fetchProfile(user.id)
  avatar <- fetchAvatar(profile.avatarId)
} yield avatar

// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
  fetchProfile(user.id).flatMap(profile =>
    fetchAvatar(profile.avatarId)))

// Map: single transformation (no chaining)
users.map(_.name)  // simple, use map

// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts)  // List[Post]

// For without yield: side effects
for (user <- users) {
  saveToDb(user)
  sendEmail(user)
}

// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yield
14

Scala 3: Given y Using

Instancias Given (reemplazando implicit val)

Scala 3 reemplaza implicit val/def con 'given'. Los givens son más claros y explícitos. Los givens anónimos (given Type = ...) tienen nombres generados por el compilador. 'given T with' define instancias con múltiples métodos. Los givens condicionales (given [A: Ordering]: Ordering[List[A]]) reemplazan implicit defs. Los givens en companion objects están en ámbito implícito automáticamente, igual que Scala 2. El cambio de palabra clave reduce la sobrecarga de 'implicit' (que significaba 4 cosas en Scala 2).

scala
// Scala 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global

// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global

// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global

// Given with 'with' for complex types
given Show[Int] with
  def show(a: Int): String = a.toString

// Given in companion object (automatic scope)
object MyType:
  given Ordering[MyType] = Ordering.by(_.id)

// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
  def compare(a: List[A], b: List[A]): Int =
    a.zip(b).find((x, y) => x != y) match
      case Some((x, y)) => summon[Ordering[A]].compare(x, y)
      case None => a.length - b.length

Cláusulas Using (reemplazando parámetros implícitos)

Scala 3 reemplaza parámetros implícitos con 'using'. Esto separa los dos significados de 'implicit' (parámetros vs conversiones). Las cláusulas using se pasan explícitamente con la palabra clave 'using'. Las cotas de contexto [A: T] permanecen igual. summon[T] reemplaza implicitly[T] (nombre más claro). La palabra clave using hace obvio en los sitios de llamada cuándo está proporcionando un contexto. Esto es un cambio puramente sintáctico—la semántica es la misma que los parámetros implícitos.

scala
// Scala 2: implicit parameter
// def process[A](data: List[A])(implicit ec: ExecutionContext): Unit

// Scala 3: using
def process[A](data: List[A])(using ec: ExecutionContext): Unit =
  data.foreach(println)

// Context bound (unchanged)
def sort[A: Ordering](list: List[A]): List[A] = list.sorted

// Multiple using clauses
def log(msg: String)(using level: Level, logger: Logger): Unit =
  logger.log(level, msg)

// summon replaces implicitly
def max[A: Ordering](a: A, b: A): A =
  val ord = summon[Ordering[A]]
  if ord.gt(a, b) then a else b

// Provide explicitly with 'using'
process(data)(using myEC)
log("hello")(using Level.INFO, myLogger)

Métodos de Extensión (Scala 3)

Scala 3 reemplaza implicit classes con 'extension'—más claro y enfocado. extension (self: T) define métodos en T. Las extensiones pueden ser genéricas (extension [A]). Combinadas con using, proporcionan sintaxis de type class (42.show). Las extensiones son solo adiciones de métodos—no crean objetos wrapper (cero sobrecarga con AnyVal en Scala 2, nativo en Scala 3). Esta es la forma idiomática de añadir métodos a tipos existentes en Scala 3.

scala
// Scala 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
//   def squared: Int = self * self
// }

// Scala 3: extension
extension (self: Int)
  def squared: Int = self * self
  def times(f: => Unit): Unit = (1 to self).foreach(_ => f)

5.squared   // 25
3.times { println("hi") }

// Extension on generic type
extension [A](self: List[A])
  def takeWhileInclusive(p: A => Boolean): List[A] = ???

// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)

42.show  // uses given Show[Int]

// Multiple extensions in one block
extension (s: String)
  def isBlank: Boolean = s.trim.isEmpty
  def words: List[String] = s.split(" ").toList

Enums y ADTs (Scala 3)

Los enums de Scala 3 reemplazan sealed trait + case objects para ADTs. Son más concisos y admiten parámetros, campos y métodos. Los casos de enum pueden tener parámetros (como case classes). La coincidencia de patrones es verificada por exhaustividad. Los enums pueden ser genéricos (Option[A]). Esto unifica enums y ADTs en un constructo. Para jerarquías abiertas (extensibles), use sealed trait + case classes aún. Para enumeraciones/ADTs cerrados, enum es más limpio.

scala
// Scala 3 enum (replaces sealed trait + case objects)
enum Color:
  case Red, Green, Blue

enum HttpStatus(val code: Int):
  case Ok extends HttpStatus(200)
  case NotFound extends HttpStatus(404)
  case Error extends HttpStatus(500)

// Pattern matching (exhaustive)
def describe(c: Color): String = c match
  case Color.Red => "red"
  case Color.Green => "green"
  case Color.Blue => "blue"

// Access fields
HttpStatus.Ok.code  // 200

// Parameterized enum cases
enum Option[+A]:
  case Some(value: A)
  case None

// ADT with methods
enum Tree[+A]:
  case Leaf(value: A)
  case Node(left: Tree[A], right: Tree[A])

  def size: Int = this match
    case Leaf(_) => 1
    case Node(l, r) => l.size + r.size + 1

Definiciones de Nivel Superior e Indentación

Scala 3 admite definiciones de nivel superior—sin necesidad de envolver todo en un object. Esto simplifica la estructura de archivos (como Python/Go). La nueva sintaxis admite tanto llaves como indentación significativa (opcional). if/then reemplaza if/else con paréntesis. match puede usarse como expresión sin llaves. Estos cambios hacen Scala 3 más accesible mientras mantiene compatibilidad hacia atrás. Puede mezclar estilos—use llaves donde añadan claridad, indentación donde reduzca ruido.

scala
// Scala 3: top-level definitions (no class wrapper needed)
// File: MyMath.scala
def add(a: Int, b: Int): Int = a + b  // top-level function

val Pi: Double = 3.14159  // top-level val

type Id = Long  // top-level type alias

given Show[Int] = (a: Int) => a.toString  // top-level given

extension (i: Int) def squared: Int = i * i  // top-level extension

// Indentation-based syntax (optional, braces still work)
def factorial(n: Int): Int =
  if n <= 1 then 1
  else n * factorial(n - 1)

// Or with braces:
def factorial2(n: Int): Int = {
  if (n <= 1) 1
  else n * factorial2(n - 1)
}

// if/then, match/case without braces
val sign = if x > 0 then 1 else -1
val desc = x match
  case 0 => "zero"
  case _ => "nonzero"
15

Métodos de Extensión y Sintaxis

Implicit Classes (Scala 2)

implicit class (Scala 2.10+) añade métodos de extensión a tipos existentes. Extender AnyVal lo hace sin asignación de memoria (el compilador borra el wrapper). La clase debe tomar un único parámetro de constructor (el tipo que se extiende). Los métodos en la implicit class están disponibles en el tipo extendido. Así es como Scala enriquece Int, String, etc. Coloca las implicit classes en un package object o utility object para compartirlas. En Scala 3, usa 'extension' en su lugar.

scala
// Scala 2: implicit class for extension methods
import scala.language.implicitConversions

implicit class RichString(val s: String) extends AnyVal {
  def wordCount: Int = s.split("\\s+").length
  def slug: String = s.toLowerCase.replaceAll("[^a-z0-9]+", "-")
  def encrypt(key: Int): String = s.map(c => (c + key).toChar)
}

"Hello World".wordCount  // 2
"My Blog Post!".slug     // "my-blog-post-"
"abc".encrypt(1)         // "bcd"

// AnyVal avoids allocation (zero overhead)
// implicit class must be in a trait, class, or object

// For generic extensions
implicit class RichList[A](val list: List[A]) extends AnyVal {
  def middle: Option[A] = list.lift(list.length / 2)
}

List(1, 2, 3, 4, 5).middle  // Some(3)

Métodos de Extensión en la Práctica

Los métodos de extensión son la forma estándar de añadir utilidades a colecciones y otros tipos. distinctBy, chunked, tap son adiciones comunes. Los operadores === y =!= (vía la type class Eq) proporcionan igualdad con seguridad de tipos (a diferencia de == que permite tipos cruzados). Agrupa las extensiones en un object e impórtalas donde sean necesarias. Esto mantiene los tipos centrales limpios mientras permite métodos específicos del dominio. La biblioteca de colecciones de Scala usa este patrón extensivamente.

scala
// Common pattern: add utility methods to collections
object CollectionExtensions {
  implicit class RichSeq[A](val seq: Seq[A]) extends AnyVal {
    def chunked(size: Int): Seq[Seq[A]] =
      seq.grouped(size).toSeq

    def distinctBy[B](f: A => B): Seq[A] =
      seq.groupBy(f).values.map(_.head).toSeq

    def tap(f: A => Unit): Seq[A] = {
      seq.foreach(f)
      seq
    }
  }
}

import CollectionExtensions._

List(1, 2, 3, 4, 5).chunked(2)  // List(List(1,2), List(3,4), List(5))
List("aa", "bb", "ab").distinctBy(_.head)  // List("aa", "ab")
List(1, 2, 3).tap(println)  // prints 1,2,3, returns List(1,2,3)

// Type class syntax via extension
implicit class EqOps[A](val a: A) extends AnyVal {
  def ===(b: A)(implicit eq: Eq[A]): Boolean = eq.eqv(a, b)
  def =!=(b: A)(implicit eq: Eq[A]): Boolean = !eq.eqv(a, b)
}

Conversiones Implícitas (Usar con Cuidado)

Las conversiones implícitas convierten tipos automáticamente, pero son peligrosas—el comportamiento del código se vuelve no obvio. Scala 2.10+ requiere opt-in explícito (scala.language.implicitConversions). Prefiere los métodos de extensión (que añaden métodos sin cambiar tipos) sobre las conversiones (que cambian tipos). Usos legítimos: interoperabilidad con Java (convertir entre tipos de colecciones), construcción de DSLs. El compilador advierte sobre conversiones implícitas—aborda estas advertencias en serio. En Scala 3, given Conversion[T, U] es el mecanismo explícito.

scala
import scala.language.implicitConversions

// Implicit conversion between types
implicit def stringToInt(s: String): Int = s.toInt
val n: Int = "42"  // stringToInt("42")

// Dangerous: can cause surprising behavior
implicit def intToBoolean(n: Int): Boolean = n != 0
if (1) println("yes")  // works! (intToBoolean(1))

// Safer: use extension methods instead
extension (n: Int) def toBool: Boolean = n != 0
if (1.toBool) println("yes")

// When implicit conversions are appropriate:
// 1. Java interop (java.util.List <-> scala.List)
implicit def javaListToScala[A](jl: java.util.List[A]): List[A] =
  import scala.jdk.CollectionConverters._
  jl.asScala.toList

// 2. Backward compatibility layers
// 3. DSL construction (use sparingly)

// Enable per-file: import scala.language.implicitConversions

Programación a Nivel de Tipos

La programación a nivel de tipos codifica información en tipos que el compilador verifica. Los phantom types (parámetros de tipo no usados) rastrean estado (Open/Closed) previniendo mal uso (no se puede leer un archivo cerrado). Los naturales de Peano representan números como tipos. Esto habilita garantías de corrección en tiempo de compilación—los bugs se vuelven errores de compilación. Usado en máquinas de estados, unidades de medida, vectores con tamaño. Poderoso pero complejo—usa cuando la seguridad justifica la complejidad de tipos. Bibliotecas como shapeless (Scala 2) habilitan programación a nivel de tipos avanzada.

scala
// Phantom types: track state at type level
sealed trait State
trait Open extends State
trait Closed extends State

class File[S <: State] private (val path: String)

object File {
  def open(path: String): File[Open] = new File[Open](path)
}

def close[S <: Open](f: File[S]): File[Closed] =
  new File[Closed](f.path)  // unsafe cast internally

def read[S <: Open](f: File[S]): String = "data"
// def read[S <: Closed](f: File[S]): String  // compile error

val f = File.open("test.txt")
read(f)  // OK: f is File[Open]
val closed = close(f)
// read(closed)  // COMPILE ERROR: closed is File[Closed]

// Type-level natural numbers (Peano)
sealed trait Nat
trait Zero extends Nat
trait Succ[N <: Nat] extends Nat

// Compile-time list length check
type _0 = Zero
type _1 = Succ[_0]
type _2 = Succ[_1]

Opaque Types (Scala 3)

Los opaque types (Scala 3) crean newtypes sin overhead—a diferencia de las value classes (AnyVal), nunca hacen boxing y son verdaderamente solo el tipo subyacente en tiempo de ejecución. La distinción de tipo existe solo en tiempo de compilación, previniendo confusiones (Celsius vs Fahrenheit, UserId vs Long). Dentro del object que lo define, el tipo y su tipo subyacente son intercambiables; fuera, son distintos. Este es el patrón 'newtype' de Haskell—tipos orientados al dominio sin coste en tiempo de ejecución. Usa para IDs, unidades y primitivas del dominio.

scala
// Scala 2: value classes for zero-overhead wrappers
// case class UserId(value: Long) extends AnyVal

// Scala 3: opaque types (true zero overhead, no boxing)
object Types:
  type UserId = Long
  object UserId:
    def apply(value: Long): UserId = value

  type Email = String
  object Email:
    def apply(s: String): Email =
      require(s.contains("@"), "invalid email")
      s

import Types.*
val id: UserId = UserId(42)  // just a Long at runtime
val email: Email = Email("[email protected]")

// UserId and Long are NOT interchangeable outside the object
// def wrong(x: Long): UserId = x  // ERROR
// But inside the object, they're the same

// Newtype pattern: domain types without overhead
type Celsius = Double
type Fahrenheit = Double
// Prevents mixing up Celsius and Fahrenheit
def toF(c: Celsius): Fahrenheit = c * 9 / 5 + 32
16

Testing (ScalaTest & ScalaCheck)

Estilos de ScalaTest

ScalaTest ofrece múltiples estilos. FunSuite es el más simple (test("name") { ... }). FlatSpec es estilo BDD ("A Stack" should "..."). Matchers proporciona aserciones legibles (shouldBe, should contain, should throw). Elige un estilo por proyecto para consistencia. FunSuite es popular para tests unitarios; FlatSpec para tests enfocados en comportamiento. Todos los estilos soportan los mismos matchers y hooks de ciclo de vida. El estilo afecta la sintaxis, no las capacidades.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatest.matchers.should.Matchers

// FunSuite: simple test functions
class MyTest extends AnyFunSuite with Matchers {
  test("addition works") {
    1 + 1 should be (2)
    1 + 1 shouldBe 2
    List(1, 2, 3) should contain (2)
  }

  test("string operations") {
    "hello".length shouldBe 5
    "hello" should startWith ("he")
  }
}

// FlatSpec: BDD-style
import org.scalatest.flatspec.AnyFlatSpec
class StackSpec extends AnyFlatSpec with Matchers {
  "A Stack" should "pop values in LIFO order" in {
    val stack = Stack(1, 2, 3)
    stack.pop() shouldBe 3
  }

  it should "throw on empty pop" in {
    val stack = Stack()
    a [NoSuchElementException] shouldBe thrownBy(stack.pop())
  }
}

// WordSpec, FreeSpec, PropSpec also available

Aserciones y Matchers

Los Matchers proporcionan aserciones expresivas. shouldBe/should be para igualdad. contain, have size, have key para colecciones. startWith/endWith/include para strings. thrownBy para excepciones. Matchers personalizados (be >, be <=) para comparaciones. El DSL 'should' se lee como inglés, haciendo los tests autodocumentados. Para aserciones complejas, usa matchers personalizados o plain assert(condition). Evita encadenar demasiado los matchers—legibilidad sobre astucia.

scala
import org.scalatest.matchers.should.Matchers._

// Equality
result shouldBe 42
result should be (42)
result should equal (42)

// Collections
list should contain (5)
list should not contain (0)
list shouldBe empty
list should have size 5
list should contain inOrder (1, 2, 3)
map should contain key ("name")
map should contain value ("Alice")

// Strings
s should startWith ("Hello")
s should endWith ("world")
s should include ("lo")
s should fullyMatch regex "H.*d".r

// Exceptions
a [IOException] should be thrownBy riskyOp()
the [IOException] thrownBy riskyOp() should have message "fail"

// Custom matchers
result should be > 0
result should be <= 100

// Type checks
result shouldBe a [List[_]]
result should be an [IllegalArgumentException]

BeforeAndAfter y Fixtures

BeforeAndAfterEach ejecuta setup/teardown alrededor de cada test. Sobrescribe beforeEach/afterEach. Para gestión de recursos, el patrón loan (withDb { conn => ... }) es más limpio—asegura limpieza vía try/finally y hace el recurso explícito. ScalaTest también soporta contextos de fixture (FixtureContext) y fixtures compartidos vía traits. Prefiere el patrón loan o métodos de fixture sobre estado mutable en beforeEach—es más funcional y evita bugs de estado compartido entre tests.

scala
import org.scalatest.BeforeAndAfterEach
import org.scalatest.funsuite.AnyFunSuite

class DbTest extends AnyFunSuite with BeforeAndAfterEach {
  var conn: Connection = _

  override def beforeEach(): Unit = {
    conn = DriverManager.getConnection("jdbc:h2:mem:test")
    conn.execute("CREATE TABLE users (id INT, name VARCHAR)")
  }

  override def afterEach(): Unit = {
    conn.close()
  }

  test("insert works") {
    conn.execute("INSERT INTO users VALUES (1, 'Alice')")
    val count = conn.query("SELECT COUNT(*) FROM users")
    count shouldBe 1
  }
}

// Fixture via loan pattern
class FixtureTest extends AnyFunSuite {
  def withDb(test: Connection => Unit): Unit = {
    val conn = DriverManager.getConnection("jdbc:h2:mem:test")
    try test(conn) finally conn.close()
  }

  test("query works") {
    withDb { conn =>
      conn.execute("INSERT ...")
      // assertions
    }
  }
}

Property-Based Testing (ScalaCheck)

ScalaCheck genera entradas de test aleatorias, probando propiedades sobre muchos casos. forAll ejecuta la propiedad con 100 entradas aleatorias por defecto. whenever filtra entradas. Los tipos Gen personalizados restringen los datos generados (Gen.choose, Gen.nonEmptyListOf). Esto captura casos límite que perderías con tests basados en ejemplos (listas vacías, números negativos, valores grandes). Los tests table-driven (Table) son para casos específicos. Property-based testing es poderoso para funciones puras y transformaciones de datos—define lo que siempre debería ser cierto.

scala
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import org.scalacheck.Prop.forAll

class ListSpec extends AnyFunSuite with ScalaCheckPropertyChecks {
  // Property: reversing twice = identity
  test("reverse twice is identity") {
    forAll { (xs: List[Int]) =>
      xs.reverse.reverse shouldBe xs
    }
  }

  // Property with conditions
  test("head of sorted list is min") {
    forAll { (xs: List[Int]) =>
      whenever(xs.nonEmpty) {
        xs.sorted.head shouldBe xs.min
      }
    }
  }

  // Custom generators
  import org.scalacheck.Gen
  val smallInt = Gen.choose(1, 100)
  val nonEmptyList = Gen.nonEmptyListOf(smallInt)

  test("custom generator") {
    forAll(nonEmptyList) { (xs: List[Int]) =>
      xs should not be empty
      xs.forall(_ >= 1) shouldBe true
    }
  }

  // Table-driven tests
  test("addition table") {
    val cases = Table(
      ("a", "b", "sum"),
      (1, 2, 3),
      (10, 20, 30),
      (-1, 1, 0)
    )
    forAll(cases) { (a, b, sum) =>
      a + b shouldBe sum
    }
  }
}

Mocking y Test Doubles

Mockito (vía ScalaTestPlus) crea test doubles. mock[T] crea un mock; when(...).thenReturn(...) stubbea métodos; verify verifica interacciones. Usa mocks para aislar la unidad bajo test de dependencias (bases de datos, APIs). Los argument matchers (argThat) verifican argumentos específicos. No hagas over-mock—si estás mockeando todo, prueba la integración real en su lugar. Prefiere fakes (implementaciones en memoria) sobre mocks para dependencias complejas—son más robustos y legibles.

scala
import org.scalatest.funsuite.AnyFunSuite
import org.scalatestplus.mockito.MockitoSugar
import org.mockito.Mockito._

class UserServiceSpec extends AnyFunSuite with MockitoSugar {
  test("getUser returns user from repo") {
    // Create mock
    val repo = mock[UserRepository]
    val user = User(1, "Alice")

    // Stub: when X then Y
    when(repo.findById(1)).thenReturn(Some(user))

    val service = new UserService(repo)
    val result = service.getUser(1)

    result shouldBe Some(user)

    // Verify: was X called?
    verify(repo).findById(1)
    verify(repo, never()).findById(2)
  }

  test("createUser saves to repo") {
    val repo = mock[UserRepository]
    val service = new UserService(repo)

    service.createUser("Bob")

    // Verify with argument matcher
    verify(repo).save(argThat((u: User) => u.name == "Bob"))
  }
}

// Stubbing exceptions
when(repo.findById(99)).thenThrow(new RuntimeException("not found"))
17

Funciones de Orden Superior & FP

Funciones como Valores

Las funciones son valores en Scala—puedes almacenarlas, pasarlas y retornarlas. (A, B) => C es el tipo función. La eta-expansion (multiply _) convierte métodos en valores de función. Las funciones de orden superior (que toman/retornan funciones) habilitan abstracciones poderosas: map, filter, reduce son HOFs. La composición de funciones (andThen, compose) construye pipelines. El currying (adder(5) retorna una función) es aplicación parcial. Esta es la base de la programación funcional en Scala.

scala
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x

// Apply
add(2, 3)  // 5
square(4)  // 16

// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _  // or just 'multiply' in Scala 3
mul(3, 4)  // 12

// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2)  // 16

// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10)  // 15

// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g  // f then g: (x+1)*2
val h2 = f compose g  // g then f: (x*2)+1
h(3)  // 8
h2(3)  // 7

Currying y Aplicación Parcial

El currying divide una función en múltiples listas de parámetros, habilitando aplicación parcial (fija algunos argumentos, obtén una función para el resto). Esto ayuda a la inferencia de tipos (parámetros anteriores restringen los posteriores) y crea funciones configurables (withDb(config)). El placeholder _ aplica parcialmente: sum(1, _) crea una función. Las múltiples listas de parámetros son el mecanismo de currying de Scala. Usa currying cuando algunos argumentos son 'configuración' y otros son 'datos'.

scala
// Curried function: multiple parameter lists
def add(a: Int)(b: Int): Int = a + b
val add5: Int => Int = add(5)  // partial application
add5(10)  // 15

// Multiple parameter lists for type inference
def map[A, B](list: List[A])(f: A => B): List[B] =
  list.map(f)
map(List(1, 2, 3))(x => x * 2)  // A, B inferred from first list

// Curried form (Function types)
val curriedAdd: Int => Int => Int = a => b => a + b
curriedAdd(5)(10)  // 15

// Uncurrying
def uncurriedAdd(a: Int, b: Int): Int = a + b

// Practical: configuration via currying
def withDb(config: DbConfig)(f: Connection => Result): Result = ???
val withMyDb = withDb(myConfig) _
withMyDb { conn => /* ... */ }

// Partial application with placeholder
val sum: (Int, Int) => Int = _ + _
val addOne: Int => Int = sum(1, _)

Funciones Puras y Transparencia Referencial

Las funciones puras siempre retornan la misma salida para la misma entrada y no tienen efectos secundarios. Son fáciles de testear, razonar, paralelizar y componer. La transparencia referencial significa que puedes reemplazar una llamada de función con su resultado sin cambiar el comportamiento. Scala no impone pureza, pero bibliotecas como Cats Effect (IO monad) te permiten aislar los efectos secundarios. Core puro + IO en los bordes es una arquitectura FP común: la lógica de negocio es pura, la I/O está envuelta en IO.

scala
// Pure function: same input → same output, no side effects
def pureAdd(a: Int, b: Int): Int = a + b  // pure

// Impure: depends on external state
var counter = 0
def impureAdd(a: Int): Int = { counter += 1; a + counter }  // impure

// Impure: side effect
def impurePrint(a: Int): Int = { println(a); a }  // side effect

// Referential transparency: can replace call with result
val x = pureAdd(2, 3)
val y = x + x  // same as pureAdd(2, 3) + pureAdd(2, 3)

// Benefits of purity:
// 1. Easy to test (no setup/teardown)
// 2. Easy to reason about (no hidden state)
// 3. Parallelizable (no shared state)
// 4. Memoizable (cache results)
// 5. Composable (predictable)

// IO monad for side effects (Cats Effect)
import cats.effect.IO
val program: IO[Unit] = IO.println("hello")
val mapped: IO[String] = IO.pure("world").map(_.toUpperCase)
// Side effects captured in IO, run at edge of program

Datos Inmutables y Colecciones Persistentes

Las estructuras de datos inmutables retornan nuevas copias al modificarse, compartiendo estructura internamente para eficiencia (estructuras de datos persistentes). Las colecciones de Scala son inmutables por defecto. case class copy crea copias modificadas. Para actualizaciones profundas, las bibliotecas Lens (Monocle) proporcionan accessors componibles. La inmutabilidad elimina clases enteras de bugs (condiciones de carrera, mutaciones inesperadas) y hace el código más fácil de razonar. El coste de rendimiento es a menudo aceptable debido al structural sharing (O(log n) no O(n)).

scala
// Immutable: operations return new collections
val list1 = List(1, 2, 3)
val list2 = list1 :+ 4  // List(1, 2, 3, 4)
// list1 is unchanged: List(1, 2, 3)

// Persistent data structures: share structure (efficient)
val map1 = Map("a" -> 1, "b" -> 2)
val map2 = map1 + ("c" -> 3)  // shares structure with map1
// O(log n) due to structural sharing, not O(n) copy

// Case classes: copy for modification
case class User(name: String, age: Int)
val alice = User("Alice", 30)
val older = alice.copy(age = 31)  // new instance, alice unchanged

// Lens (Monocle library) for nested updates
import monocle.macros.GenLens
val ageLens = GenLens[User](_.age)
val updated = ageLens.modify(_ + 1)(alice)  // User("Alice", 31)

// Benefits:
// - No bugs from shared mutation
// - Easy to reason about (values don't change)
// - Free concurrency (no locks needed)
// - Undo/redo trivial (keep old versions)

Recursión y Tail Calls

La recursión de cola (donde la llamada recursiva es la última operación) es optimizada por el compilador de Scala a un loop—sin crecimiento de pila. @tailrec verifica esto en tiempo de compilación. El patrón accumulator (pasar el resultado acumulado) convierte recursión no de cola a recursión de cola. foldLeft/foldRight encapsulan patrones comunes de recursión. Para recursión profunda no de cola, usa trampolines (Cats) o reescribe con folds. Prefiere folds sobre recursión explícita para claridad—son tail-recursive e idiomáticos.

scala
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
  if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000)  // StackOverflowError!

// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
  if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000)  // works (no stack overflow)

// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive

// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap

// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally

// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
  list match
    case Nil => acc
    case head :: tail => reverse(tail, head :: acc)
18

Case Classes & ADTs

Fundamentos de Case Class

Las case classes son la principal herramienta de modelado de datos de Scala. El compilador genera equals, hashCode, toString, copy, apply, unapply (para pattern matching) y métodos accessor. Son inmutables por defecto (campos val). Usa case classes para DTOs, value objects, mensajes y entidades de dominio (cuando son inmutables). El apply del companion object permite construir sin 'new'. copy habilita actualizaciones no destructivas. Las case classes son la base de los ADTs (Algebraic Data Types).

scala
// Case class: immutable data holder with boilerplate generated
case class Person(name: String, age: Int)

val alice = Person("Alice", 30)
val bob = Person("Bob", 25)

// Auto-generated methods:
alice.toString  // "Person(Alice,30)"
alice == Person("Alice", 30)  // true (structural equality)
alice.hashCode  // based on fields

// Pattern matching
alice match
  case Person(name, age) => s"$name is $age"

// Copy with modifications
val older = alice.copy(age = 31)  // Person("Alice", 31)

// Companion object with apply (no 'new' needed)
val p = Person("Charlie", 40)  // Person.apply called

// Fields accessed directly
alice.name  // "Alice"
alice.age   // 30

// Case class with default values
case class Point(x: Double = 0, y: Double = 0)
Point()  // Point(0.0, 0.0)
Point(y = 5)  // Point(0.0, 5.0)

Sealed Traits y ADTs

Los ADTs (Algebraic Data Types) modelan datos como un conjunto cerrado de casos (sealed trait + case classes/objects). 'sealed' significa que todos los subtipos están en el mismo archivo, habilitando pattern matching exhaustivo—el compilador advierte si pierdes un caso. Añadir un nuevo caso muestra todos los lugares que necesitan actualización (refactoring seguro). Los case objects son singletons (sin parámetros). Option, List, Either son todos ADTs. Esta es la forma funcional de modelar dominios—cada variante es un caso, el comportamiento está en el pattern matching.

scala
// ADT: sealed trait + case classes/objects
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Triangle(a: Double, b: Double, c: Double) extends Shape

// Exhaustive pattern matching (compiler checks all cases)
def area(shape: Shape): Double = shape match
  case Circle(r) => math.Pi * r * r
  case Rectangle(w, h) => w * h
  case Triangle(a, b, c) =>
    val s = (a + b + c) / 2
    math.sqrt(s * (s-a) * (s-b) * (s-c))

// Adding a new case: compiler warns about non-exhaustive matches
case class Square(side: Double) extends Shape
// area now needs a Square case!

// Case objects for singletons (no parameters)
sealed trait Status
case object Active extends Status
case object Inactive extends Status
case object Pending extends Status

// Option and List are ADTs:
// sealed trait Option[+A]
// case class Some[A](value: A) extends Option[A]
// case object None extends Option[Nothing]

Características de Case Class

Las case classes pueden tener métodos, apply personalizado (para validación), implementar traits y ser genéricas. El apply personalizado en el companion object puede validar antes de la construcción (hacer estados ilegales irrepresentables). Las case classes pueden implementar traits para polimorfismo. El constructor privado (case class Email private) fuerza usar el apply del companion para validación. Esto combina modelado de datos con encapsulación—constructores que no pueden fallar silenciosamente.

scala
// Case class with methods
case class Vec2(x: Double, y: Double) {
  def +(other: Vec2): Vec2 = Vec2(x + other.x, y + other.y)
  def magnitude: Double = math.sqrt(x*x + y*y)
  def dot(other: Vec2): Double = x*other.x + y*other.y
}

Vec2(1, 2) + Vec2(3, 4)  // Vec2(4.0, 6.0)
Vec2(3, 4).magnitude  // 5.0

// Case class with custom apply (validation)
case class Email private (value: String)
object Email {
  def apply(value: String): Email = {
    require(value.contains("@"), "invalid email")
    new Email(value)  // bypass public apply
  }
}
// Email("notanemail")  // IllegalArgumentException
Email("[email protected]")  // works

// Case class implementing trait
trait Jsonable {
  def toJson: String
}
case class User(name: String, age: Int) extends Jsonable {
  def toJson: String = s"""{"name":"$name","age":$age}"""
}

// Case class with type parameters
case class Box[A](value: A) {
  def map[B](f: A => B): Box[B] = Box(f(value))
}

Pattern Matching Profundo

El pattern matching es la poderosa herramienta de destructuring de Scala. Soporta: patrones literales (0, "hello"), patrones de tipo (s: String), patrones de case class (User(name, age)), guards (if condition), patrones anidados y binding (@). El compilador verifica exhaustividad para tipos sealed. Los patrones se prueban de arriba a abajo. Usa @ para vincular el valor completo mientras extraes partes. El pattern matching reemplaza cadenas if-else, type checks y destructuring con una sintaxis unificada y legible.

scala
// Various pattern types
val x: Any = (1, "hello")

x match
  case (a: Int, b: String) => s"int $a, string $b"
  case (a, b) => s"pair: $a, $b"
  case _ => "other"

// Guards
def classify(n: Int): String = n match
  case n if n < 0 => "negative"
  case 0 => "zero"
  case n if n % 2 == 0 => "even positive"
  case _ => "odd positive"

// Case class patterns (nested)
case class Point(x: Int, y: Int)
case class Shape(center: Point, size: Int)

def isOrigin(s: Shape): Boolean = s match
  case Shape(Point(0, 0), _) => true
  case _ => false

// Named patterns (bind the whole while extracting)
case class User(name: String, age: Int)
def describe(u: User): String = u match
  case u @ User(name, age) if age < 18 => s"$name is a minor"
  case User(name, _) => s"$name is an adult"

// Type patterns
def handle(x: Any): String = x match
  case s: String => s"string: $s"
  case n: Int => s"int: $n"
  case list: List[_] => s"list of size ${list.size}"
  case None => "none"
  case _ => "unknown"

Extractores y Patrones Personalizados

Los extractores (unapply) habilitan pattern matching en cualquier tipo, no solo case classes. Esto desacopla el patrón de matching de la representación de datos. Los Regex son extractores (los grupos se vuelven bindings). Puedes escribir extractores para tipos externos (JSON, URLs) sin modificarlos. unapply retorna Option[(T1, T2, ...)] para extracción, o Boolean para matching simple. Los extractores hacen el pattern matching extensible a cualquier fuente de datos. Las case classes auto-generan unapply; los extractores personalizados añaden matching para tipos no case class.

scala
// Custom extractor via unapply
object Email {
  def unapply(str: String): Option[(String, String)] = {
    val parts = str.split("@")
    if (parts.length == 2) Some((parts(0), parts(1)))
    else None
  }
}

// Use in pattern matching
"[email protected]" match
  case Email(user, domain) => s"user: $user, domain: $domain"
  case _ => "not an email"

// Boolean extractor (no extracted values)
object Even {
  def unapply(n: Int): Boolean = n % 2 == 0
}

5 match
  case Even() => "even"
  case _ => "odd"

// Extractor with variable arity
object Pair {
  def unapply[A, B](t: (A, B)): Option[(A, B)] = Some(t._1, t._2)
}

// Regex as extractor
val Date = "(\\d{4})-(\\d{2})-(\\d{2})".r
"2024-01-15" match
  case Date(year, month, day) => s"$year/$month/$day"
  case _ => "not a date"

// Practical: parse without case classes
object Json {
  def unapply(s: String): Option[Any] =
    scala.util.Try(ujson.read(s)).toOption
}
19

Colecciones Profundo

Colecciones Inmutables

Las colecciones de Scala son inmutables por defecto. List es una lista enlazada (acceso aleatorio O(n)). Vector es un árbol con acceso O(log n). Map y Set son basados en hash. Las operaciones retornan nuevas colecciones, compartiendo estructura para eficiencia.

scala
val list = List(1, 2, 3)
val vector = Vector(1, 2, 3)  // Fast random access
val set = Set(1, 2, 3)
val map = Map("a" -> 1, "b" -> 2)
// All immutable: operations return new collections
val updated = map + ("c" -> 3)  // New map

Operaciones de Colecciones

map transforma elementos. filter selecciona. reduce combina. grouped agrupa en chunks. flatten fusiona colecciones anidadas. flatMap mapea y aplana. Todas retornan nuevas colecciones. Las colecciones lazy usan .view o .iterator para eficiencia.

scala
val nums = (1 to 10).toList
val doubled = nums.map(_ * 2)
val evens = nums.filter(_ % 2 == 0)
val sum = nums.reduce(_ + _)
val grouped = nums.grouped(3).toList  // List(List(1,2,3), List(4,5,6), ...)
val flat = List(List(1,2), List(3,4)).flatten  // List(1,2,3,4)

Pattern Matching en Colecciones

El pattern matching funciona en colecciones. :: (cons) hace match de head y tail. List(a, b, c) hace match exactamente de 3 elementos. Nil hace match de lista vacía. _ es wildcard. Útil para parsing y destructuring. El matching exhaustivo previene bugs.

scala
val list = List(1, 2, 3)
list match {
    case head :: tail => println(s"Head: $head")
    case Nil => println("Empty")
}
List(1, 2, 3) match {
    case List(a, b, c) => println(s"$a, $b, $c")
    case _ => println("Other")
}

Colecciones Mutables

Las colecciones mutables (ArrayBuffer, mutable.Set, mutable.Map) modifican in place. Más rápidas para actualizaciones frecuentes pero no thread-safe. Usa cuando el rendimiento importa y no se necesita inmutabilidad. Convierte a inmutable con .toMap, .toSet.

scala
import scala.collection.mutable
val buffer = mutable.ArrayBuffer(1, 2, 3)
buffer += 4  // Add element
buffer -= 2  // Remove element
buffer(0) = 10  // Update
val mset = mutable.Set(1, 2, 3)
val mmap = mutable.Map("a" -> 1)
mmap("b") = 2

Colecciones Lazy

view crea una vista lazy: las operaciones se difieren hasta que se fuerzan. Útil para operaciones encadenadas en colecciones grandes. LazyList (Scala 2.13+) es una secuencia lazy. Las secuencias infinitas son posibles con evaluación lazy. Fuerza con .toList, .toArray.

scala
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head  // Only computes first
val result = lazyView.take(10).toList  // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)
20

Implicits

Parámetros Implícitos

Los parámetros implícitos se pasan automáticamente cuando están en scope. El compilador busca un valor implícito que coincida. Usado para configuración, type classes y contexto. Puede ser sobreescrito explícitamente. Múltiples implícitos deben tener tipos distintos.

scala
def connect(url: String)(implicit timeout: Int): Unit = {
    println(s"Connecting to $url with timeout $timeout")
}
implicit val defaultTimeout: Int = 5000
connect("http://example.com")  // Uses 5000
connect("http://example.com")(3000)  // Explicit override

Conversiones Implícitas

Las conversiones implícitas convierten tipos automáticamente. Pueden ser peligrosas (conversiones inesperadas). implicit class añade métodos de extensión. Scala 3 usa given/using y extension para claridad. Prefiere métodos de extensión sobre conversiones raw.

scala
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42  // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
    def squared: Int = n * n
}
5.squared  // 25

Type Classes

Las type classes son un patrón para polimorfismo ad-hoc. Un trait define comportamiento, las instancias proporcionan implementaciones para tipos específicos. La resolución implícita encuentra la instancia. Más flexible que la herencia. Común en Cats y Shapeless.

scala
trait Show[A] {
    def show(a: A): String
}
object Show {
    implicit val intShow: Show[Int] = (a: Int) => a.toString
    implicit val stringShow: Show[String] = (a: String) => a
    def apply[A](a: A)(implicit s: Show[A]): String = s.show(a)
}
Show(42)  // "42"
Show("hello")  // "hello"

Context Bounds

Los context bounds [A: TypeClass] son syntactic sugar para parámetros implícitos. La instancia de la type class está disponible vía implicitly. Sintaxis más limpia para restricciones de type class. Común con Ordering, Numeric y type classes personalizadas.

scala
def max[A: Ordering](a: A, b: A): A = {
    val ord = implicitly[Ordering[A]]
    if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
    if (ord.gt(a, b)) a else b

Given/Using (Scala 3)

Scala 3 reemplaza los implícitos con given/using para claridad. given define una instancia, using declara un parámetro. extension reemplaza implicit class. Más explícito y legible. Las herramientas de migración convierten los implícitos de Scala 2.

scala
// Scala 3 syntax
given defaultTimeout: Int = 5000
def connect(url: String)(using timeout: Int): Unit =
    println(s"Connecting with $timeout")
connect("http://example.com")  // Uses given
// Extension methods
extension (n: Int)
    def squared: Int = n * n
21

Concurrencia

Future

Future representa una computación async. onComplete maneja la finalización. map/flatMap encadenan operaciones. ExecutionContext proporciona threads. Los Futures son inmutables y de un solo uso. Usa for-comprehension para múltiples futures.

scala
import scala.concurrent.{Future, ExecutionContext}
import ExecutionContext.Implicits.global
val f: Future[Int] = Future {
    Thread.sleep(1000)
    42
}
f.onComplete {
    case Success(v) => println(v)
    case Failure(e) => println(e)
}
// Map/flatMap for chaining
val f2 = f.map(_ * 2).flatMap(x => Future(x + 1))

for-comprehension con Futures

for-comprehension se desugarea a flatMap/map. Secuencial: cada paso espera al anterior. Para ejecución paralela, inicia los futures antes del for. Mucho más legible que callbacks anidados. Funciona con cualquier monad (Future, Option, List).

scala
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
    user <- getUser(1)
    orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chaining

Futures Paralelos

Iniciar los futures antes del for-comprehension los ejecuta en paralelo. Future.sequence convierte List[Future[T]] a Future[List[T]]. Future.traverse mapea y secuencial en un paso. zip combina dos futures. Todos completan cuando el más lento termina.

scala
val f1 = Future { compute1() }
val f2 = Future { compute2() }
val combined: Future[(Int, Int)] = for {
    r1 <- f1
    r2 <- f2
} yield (r1, r2)
// Or: Future.sequence(List(f1, f2))
// Or: Future.traverse(list)(compute)

Actors (Akka)

Los actors de Akka encapsulan estado y se comunican vía mensajes. Sin estado mutable compartido. Cada actor procesa un mensaje a la vez. ! (tell) envía fire-and-forget. ? (ask) retorna un Future. La supervision maneja fallos. Ideal para sistemas con estado concurrentes.

scala
import akka.actor.*
class Counter extends Actor {
    var count = 0
    def receive = {
        case "inc" => count += 1
        case "get" => sender() ! count
    }
}
val system = ActorSystem("mySystem")
val counter = system.actorOf(Props[Counter], "counter")
counter ! "inc"
counter ! "get"

Cats Effect IO

Cats Effect IO es una IO monad funcional pura. Referencialmente transparente: IO(println("x")) es un valor. Compone con for-comprehension. Cancelable y resource-safe. unsafeRunSync se ejecuta en el borde de tu programa. Alternativa a Future con mejores semánticas.

scala
import cats.effect.IO
val program: IO[Int] = for {
    _ <- IO(println("Start"))
    result <- IO.pure(42)
    _ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellable
22

Pattern Matching Profundo

Case Classes

Las case classes auto-generan equals, hashCode, toString y apply/unapply. Perfectas para pattern matching. El pattern matching las destructurea. Inmutables por defecto. Usa para algebraic data types. copy() crea copias modificadas.

scala
case class Point(x: Int, y: Int)
val p = Point(1, 2)
p match {
    case Point(0, 0) => "origin"
    case Point(0, _) => "on y-axis"
    case Point(x, 0) => s"on x-axis at $x"
    case Point(x, y) => s"at ($x, $y)"
}

Sealed Traits

Los sealed traits restringen los subtipos al mismo archivo. El compilador verifica exhaustividad en pattern matching. Añadir un nuevo subtipo causa advertencias en todos los matches. Ideal para jerarquías de tipos cerradas. Combinado con case classes para ADTs.

scala
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
    case Circle(r) => math.Pi * r * r
    case Square(s) => s * s
    case Rectangle(w, h) => w * h
}  // Compiler warns if not exhaustive

Guards & Extractores

Los guards (if) añaden condiciones a los patrones. Los extractores personalizados (unapply) habilitan pattern matching en cualquier tipo. unapply retorna Option para indicar match. Los extractores desacoplan el matching del tipo. Poderoso para DSLs y parsing.

scala
def classify(n: Int): String = n match {
    case x if x < 0 => "negative"
    case 0 => "zero"
    case x if x % 2 == 0 => "even"
    case _ => "odd"
}
// Custom extractor
object Even {
    def unapply(n: Int): Option[Int] =
        if (n % 2 == 0) Some(n / 2) else None
}
4 match { case Even(half) => s"half is $half" }

Funciones Parciales

Las funciones parciales están definidas solo para algunas entradas. isDefinedAt verifica. collect aplica solo donde está definida. Útil para callbacks y routing. orElse combina funciones parciales. lift convierte a función total retornando Option.

scala
val pf: PartialFunction[Int, String] = {
    case 1 => "one"
    case 2 => "two"
}
pf.isDefinedAt(1)  // true
pf.isDefinedAt(3)  // false
// Collect = filter + map
List(1, 2, 3, 1).collect(pf)  // List("one", "two", "one")

Tipos de Pattern Matching

El pattern matching funciona en tipos, pero el type erasure afecta a los genéricos. List(a, b) hace match de una lista de 2 elementos. List(_*) hace match de cualquier lista. Evita hacer match en tipos genéricos como List[Int] (erased). Usa type tags para información de tipo en tiempo de ejecución.

scala
def describe(x: Any): String = x match {
    case i: Int => s"Int: $i"
    case s: String => s"String: $s"
    case List(a, b) => s"Two-element list: $a, $b"
    case List(_*) => "List with elements"
    case Some(v) => s"Some: $v"
    case None => "None"
    case _ => "Unknown"
}
23

Programación Funcional

Funciones de Orden Superior

Las funciones de orden superior toman o retornan funciones. El currying divide funciones multi-argumento en cadenas de un solo argumento. La aplicación parcial fija algunos argumentos. Habilita composición de funciones y reutilización. _ crea una función parcialmente aplicada.

scala
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5)  // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _  // Partial application
add5(3)  // 8

Option & Either

Option representa valores opcionales: Some o None. Evita null. getOrElse proporciona un valor por defecto. Either representa éxito (Right) o fallo (Left). Mejor que excepciones para errores esperados. Ambos son monads: map, flatMap, for-comprehension.

scala
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
find(1).getOrElse("Unknown")  // "Alice"
find(-1).getOrElse("Unknown")  // "Unknown"
// Either for error handling
def parse(s: String): Either[String, Int] =
    try Right(s.toInt)
    catch { case _: Exception => Left(s"Not a number: $s") }

Composición de Funciones

compose encadena funciones de derecha a izquierda (como matemáticas). andThen encadena de izquierda a derecha (más legible). Ambas crean nuevas funciones. Útil para construir pipelines. Las funciones son valores first-class en Scala.

scala
val addOne: Int => Int = _ + 1
val double: Int => Int = _ * 2
// Compose (right to left)
val f = addOne compose double  // double then addOne
f(3)  // 7
// AndThen (left to right)
val g = addOne andThen double  // addOne then double
g(3)  // 8

Recursión y Recursión de Cola

La recursión de cola se optimiza a un loop por el compilador. La llamada recursiva debe ser la última operación. La anotación @tailrec verifica esto en tiempo de compilación. El patrón accumulator lleva el estado. Previene stack overflow para recursión profunda.

scala
// Not tail-recursive: stack overflow for large n
def factorial(n: Int): Int =
    if (n <= 1) 1 else n * factorial(n - 1)
// Tail-recursive: optimized to loop
import scala.annotation.tailrec
@tailrec
def factorial(n: Int, acc: Int = 1): Int =
    if (n <= 1) acc else factorial(n - 1, n * acc)

Monads

Los monads tienen flatMap y unit (pure/pure). Encadenan operaciones con contexto (Option: ausencia, List: no determinismo, Future: async). for-comprehension se desugarea a flatMap/map. Las leyes de los monads aseguran composición correcta. Cats proporciona la type class Monad.

scala
// Monad laws: left identity, right identity, associativity
// Option is a monad:
Some(5).flatMap(x => Some(x + 1))  // Some(6)
None.flatMap(x => Some(x + 1))  // None
// List is a monad:
List(1, 2).flatMap(x => List(x, x * 10))  // List(1, 10, 2, 20)
// for-comprehension is monadic sugar:
for {
    x <- Some(5)
    y <- Some(x + 1)
} yield y  // Some(6)
24

Pitfalls Comunes

Null vs Option

Scala tiene null para compatibilidad con Java pero se desaconseja. Option representa ausencia explícitamente. El pattern matching fuerza manejar None. Usa .toOption en Try para excepciones. Evita null en código Scala; resérvalo para interoperabilidad con Java.

scala
// BAD: null
def find(id: Int): String =
    if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
    if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic Scala

Var vs Val

val es inmutable (valor), var es mutable (variable). Prefiere val para código más seguro y predecible. El estado mutable complica el razonamiento y la concurrencia. Usa var solo para rendimiento local o cuando sea realmente necesario. Las colecciones son inmutables por defecto.

scala
var x = 1  // Mutable
x = 2  // OK
val y = 1  // Immutable
// y = 2  // Error
// Prefer val for immutability
// Use var only when necessary
// Mutable state causes bugs in concurrent code

Igualdad

Scala == llama a equals (igualdad de valor), a diferencia de Java. eq verifica igualdad de referencia. ne es la negación de eq. Siempre usa == para comparación de valor. Las case classes tienen equals correcto. Para clases personalizadas, sobrescribe equals y hashCode.

scala
val a = List(1, 2)
val b = List(1, 2)
a == b  // true (value equality)
a eq b  // false (reference equality)
// In Java: a == b compares references
// In Scala: == calls equals (value)
// Use eq for reference equality (rarely needed)

Ambigüedad de Implícitos

Múltiples implícitos del mismo tipo causan errores de ambigüedad. Mantén el scope implícito limpio. Usa tipos específicos (newtypes) para distinguir. given/using de Scala 3 es más claro. Evita conversiones implícitas; causan comportamiento sorprendente.

scala
implicit val s1: String = "hello"
implicit val s2: String = "world"
def greet(implicit s: String) = println(s)
greet  // Error: ambiguous implicit values
// Fix: only one implicit in scope
// Or: pass explicitly
greet(s1)

By-Name vs By-Value

Los parámetros by-name (=> T) se evalúan lazy, cada vez que se usan. Los parámetros by-value se evalúan una vez antes de la llamada. By-name habilita estructuras de control personalizadas (unless, while). Puede causar múltiples evaluaciones; cachea con lazy val si es necesario.

scala
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation())  // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation())  // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
    if (!cond) body
25

Akka Actors

Actor System

ActorSystem es el contenedor de actors. actorOf crea actors con un nombre. ! (tell) envía un mensaje asincrónicamente. ? (ask) envía y retorna un Future. Los actors se identifican por path. terminate apaga el sistema graceful.

scala
import akka.actor.*
val system = ActorSystem("MySystem")
val actor = system.actorOf(Props[MyActor], "myActor")
actor ! "Hello"  // Fire-and-forget
val future = actor ? "Query"  // Ask pattern
system.terminate()

Ciclo de Vida del Actor

preStart se ejecuta al crear, postStop al terminar. preRestart/postRestart manejan supervision. El actor se detiene antes de reiniciarse, luego se inicia fresh. El estado interno se pierde al reiniciar. Usa preStart para inicialización y postStop para limpieza.

scala
class MyActor extends Actor {
    override def preStart(): Unit = println("Starting")
    override def postStop(): Unit = println("Stopped")
    override def preRestart(reason: Throwable, message: Option[Any]): Unit = {
        println("Restarting")
        super.preRestart(reason, message)
    }
    def receive = {
        case "ping" => sender() ! "pong"
    }
}

Supervision

La supervision define cómo se manejan los fallos. Resume: continuar con el mismo estado. Restart: recrear con estado fresh. Stop: terminar permanentemente. Escalate: dejar que el parent lo maneje. OneForOneStrategy afecta solo al child fallido. AllForOneStrategy afecta a todos los children.

scala
class Supervisor extends Actor {
    override val supervisorStrategy = OneForOneStrategy() {
        case _: ArithmeticException => Resume  // Continue
        case _: NullPointerException => Restart  // Restart actor
        case _: Exception => Stop  // Stop actor
    }
    def receive = { case _ => }
}

Routers

Los routers distribuyen mensajes a múltiples actors. Pool crea y gestiona workers. Group usa actors existentes. Estrategias: RoundRobin, Random, SmallestMailbox, ConsistentHashing, ScatterGatherFirst. Los routers mejoran throughput paralelizando trabajo.

scala
// Pool router: creates workers
val router = system.actorOf(
    RoundRobinPool(5).props(Props[Worker]),
    "router"
)
router ! "work"  // Distributed to one of 5 workers
// Group router: uses existing actors
val group = system.actorOf(
    RoundRobinGroup(paths).props(),
    "group"
)

Persistence

PersistentActor guarda eventos en un journal (event sourcing). Los Commands se validan, luego se persisten como events. Al reiniciar, los events se reproducen vía receiveRecover. El estado se reconstruye desde los events. Habilita recuperación de crashes. Usa EventSourcedBehavior para Akka Typed.

scala
class Counter extends PersistentActor {
    var count = 0
    override def persistenceId = "counter-1"
    override def receiveCommand = {
        case "inc" => persist(Incremented) { event =>
            count += 1
        }
    }
    override def receiveRecover = {
        case Incremented => count += 1
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.