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).
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.StringInterpolació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.
val name = "Alice"
val age = 30
println(s"Name: ${name}, Age: ${age}")
println(s"Next year: ${age + 1}")
println(s"Upper: ${name.toUpperCase}")
println(f"${name}%s weighs ${65.5}%.1f kg") // formatted
println(raw"No \n escape") // raw stringTuplas
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.
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.0Inferencia 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.
val x = 42 // Int inferred
val y: Long = 42 // explicit Long
val z = 42: Long // type ascription
val list = List(1, 2, 3) // List[Int]
val mixed: List[Any] = List(1, "a", true)
def double(x: Int) = x * 2 // return type inferredUnit 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.
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")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.
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")) // trueCadenas 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.
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
WorldConstrucció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).
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.
val n = "42".toInt // 42
val d = "3.14".toDouble // 3.14
val b = "true".toBoolean // true
val safe = "abc".toIntOption // Some(42) or None
// Handling errors
val result = try "x".toInt catch { case _ => 0 }
println(result) // 0Regex
.r 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ó.
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#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.
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.
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.
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.
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.
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, fastestFlujo 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.
val score = 85
val grade =
if (score >= 90) "A"
else if (score >= 80) "B"
else if (score >= 70) "C"
else "F"
println(grade) // B
// if returns a value—no ternary operator neededFor Comprehension
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.
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.
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.
var i = 0
while (i < 3) {
println(i)
i += 1
}
var j = 0
do {
println(j)
j += 1
} while (j < 3)
// Prefer recursion or for-comprehensions for immutabilityTry / Catch / Finally
try/catch/finally 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.
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) // 0Funciones
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).
def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
s"${greeting}, ${name}!"
def log(msg: String): Unit = println(msg)
println(add(3, 4)) // 7
println(greet("Alice")) // Hello, Alice!
println(greet("Bob", greeting = "Hi")) // named argLambda (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.
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 parameterFunciones 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.
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(applyTwice(_ + 3, 5)) // 11
def makeAdder(n: Int): Int => Int = _ + n
val add5 = makeAdder(5)
println(add5(10)) // 15
// Functions returning functions = currying-likeCurrying 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.
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.
// 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 }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.
class Person(val name: String, val age: Int) {
// Constructor params with val/var become fields
def greet: String = s"Hi, I'm ${name}"
def isAdult: Boolean = age >= 18
}
val p = new Person("Alice", 30)
println(p.greet) // Hi, I'm Alice
println(p.name) // Alice (val field)
println(p.isAdult) // trueCase Class
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.
case class Point(x: Int, y: Int)
val p1 = Point(3, 4) // no 'new' needed
val p2 = Point(3, 4)
println(p1 == p2) // true (value equality)
val moved = p1.copy(x = 5) // Point(5, 4)
println(p1.x, p1.y) // 3 4 (fields auto-visible)
// Auto: equals, hashCode, toString, copy, companionTraits (Interfaces 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.
trait Greetable {
def name: String // abstract
def greet: String = s"Hello, ${name}" // concrete
}
trait Named {
val name: String
}
class User(val name: String) extends Greetable
val u = new User("Alice")
println(u.greet) // Hello, Alice
// Stackable traits via linearizationObject (Singleton)
object declara 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.
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.
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 mixinsColecciones 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.
val nums = List(1, 2, 3, 4, 5)
println(nums.map(_ * 2)) // List(2,4,6,8,10)
println(nums.filter(_ % 2 == 0)) // List(2,4)
println(nums.reduce(_ + _)) // 15
println(nums.foldLeft(0)(_ + _)) // 15
println(nums.sum) // 15
println(nums.mkString(", ")) // 1, 2, 3, 4, 5FlatMap 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.
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.
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.
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.
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(_ + _)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.
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.
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 patternsCoincidencia 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.
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.
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.
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")
}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.
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.
// 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.
def greet(name: String)(implicit greeting: String): String =
s"${greeting}, ${name}!"
implicit val defaultGreeting: String = "Hello"
println(greet("Alice")) // Hello, Alice! (implicit injected)
println(greet("Bob")("Hi")) // Hi, Bob! (explicit override)
// Compiler finds implicit in scopeType Classes (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.
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 polymorphismMé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.
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 * nConcurrencia 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.
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-comprehensionsComposició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.
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.
val nums = (1 to 1000000).toList
val sum = nums.par.sum // parallel sum
println(sum)
val doubled = nums.par.map(_ * 2).toList
// .par converts to ParCollection
// Operations run on multiple threads
// Use for CPU-bound work on large collectionsPromise (Future Manual)
Promise 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.
import scala.concurrent.{Promise, Future, ExecutionContext}
import ExecutionContext.Implicits.global
val p = Promise[Int]()
val f = p.future
// Complete the promise from another thread
Future { Thread.sleep(100); p.success(42) }
f.foreach(println) // 42 (when complete)
// p.failure(new Exception) for errors
// Promise = write side, Future = read sideSync vs Async (Await)
Await.result 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.
import scala.concurrent.{Future, Await}
import scala.concurrent.duration._
import ExecutionContext.Implicits.global
val f = Future { Thread.sleep(500); 42 }
// Block and wait (use sparingly—mainly in tests)
val result = Await.result(f, 1.second)
println(result) // 42
// Await.ready returns Try, Await.result returns value
// Avoid in production servers—use callbacks insteadImplicits 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.
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.
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 // convertedPrioridad 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.
// 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.
// 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'.
// Implicit in companion object is found automatically
case class UserId(value: Long)
object UserId {
implicit val ordering: Ordering[UserId] =
Ordering.by(_.value)
}
// No import needed—companion object implicits are in scope
List(UserId(3), UserId(1), UserId(2)).sorted
// Works because Ordering[UserId] is in UserId's companion
// Implicit scope includes:
// 1. Companion object of the type (UserId)
// 2. Companion object of the type class (Ordering)
// 3. Companion objects of type parameters
// This is why Int has an Ordering:
// object Int { implicit val ord: Ordering[Int] = ... }
// Package object for shared implicits
package object myapp {
implicit val ec: ExecutionContext = ExecutionContext.global
type Id = Long
}Type Classes
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.
// 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.
// Context bound syntax
def printAll[A: Show](items: List[A]): Unit =
items.foreach(a => println(summon[Show[A]].show(a)))
// Extension methods via implicit class
implicit class ShowOps[A](val a: A) extends AnyVal {
def show(implicit s: Show[A]): String = s.show(a)
}
42.show // "42"
"hi".show // ""hi""
List(1,2).show // "[1, 2]"
// Combining: type class + extension methods
def format[A: Show](a: A): String = a.show
// Standard library type classes
def sum[A: Numeric](xs: List[A]): A =
summon[Numeric[A]].plus(xs.head, xs.tail.foldLeft(
summon[Numeric[A]].zero)(summon[Numeric[A]].plus))
// Or with syntax:
import Numeric.Implicits._
def sum2[A: Numeric](xs: List[A]): A = xs.sumType Classes 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.
// 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.
// 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 correctlyDerivació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 3: derive type class instances automatically
import scala.deriving.Mirror
trait Show[A] {
def show(a: A): String
}
object Show {
// Inline given for derivation
given showInt: Show[Int] with
def show(a: Int) = a.toString
given showString: Show[String] with
def show(s: String) = s""$s""
// Derive for products (case classes)
given showProduct[A](using m: Mirror.ProductOf[A])
(using ev: Show[m.MirroredElemTypes]): Show[A] with
def show(a: A): String = ???
// Or use Scala 3's derivation
inline given derive[A](using m: Mirror.Of[A]): Show[A] = ???
}
// Auto-derive for case classes
case class Person(name: String, age: Int) derives Show
// Show[Person] is generated automaticallyFor Comprehensions 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.
// For comprehension: syntactic sugar for flatMap/map
val result = for {
x <- List(1, 2, 3)
y <- List(10, 20)
} yield x + y
// List(11, 21, 12, 22, 13, 23)
// Desugared:
List(1, 2, 3).flatMap { x =>
List(10, 20).map { y => x + y }
}
// With filters (if guards)
val evens = for {
x <- 1 to 10
if x % 2 == 0
} yield x
// Vector(2, 4, 6, 8, 10)
// Desugared:
(1 to 10).withFilter(_ % 2 == 0).map(identity)
// Without yield: imperative (foreach)
for (x <- 1 to 3) println(x) // 1, 2, 3For 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.
// Option: chain operations that might return None
def getUser(id: Int): Option[User] = ???
def getEmail(user: User): Option[String] = ???
val email: Option[String] = for {
user <- getUser(42)
email <- getEmail(user)
} yield email
// Desugared:
getUser(42).flatMap(user => getEmail(user).map(email => email))
// Future: chain async operations
val result: Future[Int] = for {
user <- fetchUser(1) // Future[User]
posts <- fetchPosts(user) // Future[List[Post]]
} yield posts.size
// If any returns None/failed Future, the whole chain short-circuits
// This is the power of monadic compositionFor 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.
// 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.
// For-comprehensions require flatMap, map, withFilter
// Define your own monad to use for-comprehensions
case class Box[A](value: A) {
def map[B](f: A => B): Box[B] = Box(f(value))
def flatMap[B](f: A => Box[B]): Box[B] = f(value)
def withFilter(p: A => Boolean): Box[A] =
if (p(value)) this else throw new NoSuchElementException
}
val result = for {
x <- Box(10)
y <- Box(20)
if x < y
} yield x + y
// Box(30)
// Desugaring steps:
// 1. Box(10).flatMap { x =>
// 2. Box(20).withFilter(_ > x... wait, order matters
// Actual:
// Box(10).flatMap(x =>
// Box(20).withFilter(y => x < y).map(y => x + y))
// Assignment within for (= instead of <-)
for {
x <- List(1, 2, 3)
doubled = x * 2 // local val
} yield doubledFor vs Map/FlatMap (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.
// For: sequential, dependent operations
val result = for {
user <- fetchUser(id)
profile <- fetchProfile(user.id)
avatar <- fetchAvatar(profile.avatarId)
} yield avatar
// Equivalent with flatMap (harder to read):
fetchUser(id).flatMap(user =>
fetchProfile(user.id).flatMap(profile =>
fetchAvatar(profile.avatarId)))
// Map: single transformation (no chaining)
users.map(_.name) // simple, use map
// FlatMap: when you need to chain but for is overkill
users.flatMap(_.posts) // List[Post]
// For without yield: side effects
for (user <- users) {
saveToDb(user)
sendEmail(user)
}
// Guidelines:
// - 1 operation: use map/flatMap directly
// - 2+ dependent operations: use for-comprehension
// - Side effects: use for without yieldScala 3: Given 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 2: implicit val
// implicit val ec: ExecutionContext = ExecutionContext.global
// Scala 3: given
given ec: ExecutionContext = ExecutionContext.global
// Anonymous given (inferred name)
given ExecutionContext = ExecutionContext.global
// Given with 'with' for complex types
given Show[Int] with
def show(a: Int): String = a.toString
// Given in companion object (automatic scope)
object MyType:
given Ordering[MyType] = Ordering.by(_.id)
// Conditional given (like implicit def)
given [A: Ordering]: Ordering[List[A]] with
def compare(a: List[A], b: List[A]): Int =
a.zip(b).find((x, y) => x != y) match
case Some((x, y)) => summon[Ordering[A]].compare(x, y)
case None => a.length - b.lengthCláusulas Using (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 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 2: implicit class
// implicit class RichInt(val self: Int) extends AnyVal {
// def squared: Int = self * self
// }
// Scala 3: extension
extension (self: Int)
def squared: Int = self * self
def times(f: => Unit): Unit = (1 to self).foreach(_ => f)
5.squared // 25
3.times { println("hi") }
// Extension on generic type
extension [A](self: List[A])
def takeWhileInclusive(p: A => Boolean): List[A] = ???
// Extension with using (type class syntax)
extension [A](self: A) def show(using s: Show[A]): String = s.show(self)
42.show // uses given Show[Int]
// Multiple extensions in one block
extension (s: String)
def isBlank: Boolean = s.trim.isEmpty
def words: List[String] = s.split(" ").toListEnums 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 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 + 1Definiciones 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 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"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 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.
// 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.
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.implicitConversionsProgramació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.
// 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 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 + 32Testing (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.
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 availableAserciones 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.
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.
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.
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.
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"))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.
// Functions are first-class values
val add: (Int, Int) => Int = (a, b) => a + b
val square: Int => Int = x => x * x
// Apply
add(2, 3) // 5
square(4) // 16
// Method to function (eta-expansion)
def multiply(a: Int, b: Int): Int = a * b
val mul = multiply _ // or just 'multiply' in Scala 3
mul(3, 4) // 12
// Higher-order: function taking function
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(square, 2) // 16
// Higher-order: function returning function
def adder(n: Int): Int => Int = _ + n
val add5 = adder(5)
add5(10) // 15
// Function composition
val f: Int => Int = _ + 1
val g: Int => Int = _ * 2
val h = f andThen g // f then g: (x+1)*2
val h2 = f compose g // g then f: (x*2)+1
h(3) // 8
h2(3) // 7Currying 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'.
// 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.
// 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 programDatos 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)).
// 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.
// Regular recursion (can stack overflow)
def factorial(n: Int): Int =
if (n <= 1) 1 else n * factorial(n - 1)
factorial(10000) // StackOverflowError!
// Tail recursion: compiler optimizes to loop
import scala.annotation.tailrec
@tailrec
def factorialTail(n: Int, acc: Int = 1): Int =
if (n <= 1) acc else factorialTail(n - 1, n * acc)
factorialTail(10000) // works (no stack overflow)
// @tailrec annotation: compiler verifies it's tail-recursive
// Error if not actually tail-recursive
// Trampoline for non-tail recursion
// (Cats' Trampoline or Free monads)
// Converts stack recursion to heap
// Fold as alternative to recursion
def sum(list: List[Int]): Int = list.foldLeft(0)(_ + _)
// foldLeft is tail-recursive internally
// Pattern: accumulator pattern
@tailrec
def reverse[A](list: List[A], acc: List[A] = Nil): List[A] =
list match
case Nil => acc
case head :: tail => reverse(tail, head :: acc)Case Classes & 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).
// 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.
// 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.
// 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.
// 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.
// 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
}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.
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 mapOperaciones 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.
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.
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.
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") = 2Colecciones 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.
val lazyView = (1 to 1000000).view.map(_ * 2).filter(_ > 100)
// No computation yet
val first = lazyView.head // Only computes first
val result = lazyView.take(10).toList // Only 10 elements processed
// LazyList (formerly Stream)
val fibs: LazyList[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map(_ + _)Implicits
Parámetros Implícitos
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.
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 overrideConversiones 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.
import scala.language.implicitConversions
implicit def intToString(n: Int): String = n.toString
val s: String = 42 // Converts via intToString
// Extension methods (Scala 3 preferred)
implicit class RichInt(val n: Int) extends AnyVal {
def squared: Int = n * n
}
5.squared // 25Type Classes
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.
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.
def max[A: Ordering](a: A, b: A): A = {
val ord = implicitly[Ordering[A]]
if (ord.gt(a, b)) a else b
}
// Equivalent to:
def max2[A](a: A, b: A)(implicit ord: Ordering[A]): A =
if (ord.gt(a, b)) a else bGiven/Using (Scala 3)
Scala 3 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 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 * nConcurrencia
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.
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).
def getUser(id: Int): Future[User] = ...
def getOrders(user: User): Future[List[Order]] = ...
val result: Future[List[Order]] = for {
user <- getUser(1)
orders <- getOrders(user)
} yield orders
// Equivalent to flatMap/map chainingFutures Paralelos
Iniciar 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.
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.
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.
import cats.effect.IO
val program: IO[Int] = for {
_ <- IO(println("Start"))
result <- IO.pure(42)
_ <- IO(println(s"Got $result"))
} yield result
// Run at end of world
program.unsafeRunSync()
// Referentially transparent, cancellablePattern Matching Profundo
Case Classes
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.
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.
sealed trait Shape
case class Circle(radius: Double) extends Shape
case class Square(side: Double) extends Shape
case class Rectangle(w: Double, h: Double) extends Shape
def area(s: Shape): Double = s match {
case Circle(r) => math.Pi * r * r
case Square(s) => s * s
case Rectangle(w, h) => w * h
} // Compiler warns if not exhaustiveGuards & 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.
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.
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.
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"
}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.
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
applyTwice(_ + 3, 5) // 11
// Currying
def add(a: Int)(b: Int): Int = a + b
val add5 = add(5) _ // Partial application
add5(3) // 8Option & 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.
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.
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) // 8Recursió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.
// 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.
// 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)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.
// BAD: null
def find(id: Int): String =
if (id > 0) "Alice" else null
// GOOD: Option
def find(id: Int): Option[String] =
if (id > 0) Some("Alice") else None
// Scala avoids null; use Option
// NullPointer exceptions are rare in idiomatic ScalaVar vs Val
val 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.
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 codeIgualdad
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.
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.
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.
// By-value: evaluated once
def log(msg: String) = println(msg)
log(expensiveComputation()) // Evaluated before call
// By-name: evaluated each use
def log(msg: => String) = println(msg)
log(expensiveComputation()) // Evaluated only if called
// Useful for lazy evaluation and control flow
def unless(cond: Boolean)(body: => Unit): Unit =
if (!cond) bodyAkka Actors
Actor System
ActorSystem 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.
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.
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.
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.
// 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.
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
}
}Fragmentos de Scala relacionados
Copy-paste ready code for common tasks.
Coincidencia de Patrones
Desestructurar valores y coincidir casos en Scala.
Case Classes
Clases de datos inmutables con equals/hashCode/toString autogenerados.
Operaciones de Colecciones
Operaciones funcionales: map, filter, fold, groupBy.
Traits y Mixins
Componer comportamientos usando traits con implementaciones por defecto.
Futures y Async
Computación asíncrona con Future y ExecutionContext.
Implicits (Given/Using en Scala 3)
Derivación de type classes y paso de contexto vía implicits.
Akka Actors (Pekko)
Concurrencia de paso de mensajes con el modelo actor.
Type Classes (estilo Cats)
Polimorfismo ad-hoc vía type classes.
Was this helpful?