cv.cm

Programación · Lenguajes de programación

Tutorial de Scala para principiantes

7 min de lectura

Respuesta corta

Scala es un lenguaje de la JVM con tipado estático que combina programación orientada a objetos y funcional, muy usado en ingeniería de datos con Apache Spark y en backends concurrentes. Sus rasgos clave son la inferencia de tipos, las colecciones inmutables, las case classes y el pattern matching. Esta página usa Scala 3 para cubrir la instalación, los valores, el control de flujo, las funciones, las colecciones, los traits, los enums y el manejo de errores con Option, Either y Try.

Qué es Scala y para qué se usa

Scala es un lenguaje de tipado estático que combina programación orientada a objetos y funcional. Lo creó Martin Odersky en la EPFL y se ejecuta sobre todo en la máquina virtual de Java (JVM), con Scala.js para el navegador y Scala Native para binarios nativos. El código Scala puede usar cualquier biblioteca de Java.

Scala se usa mucho en ingeniería de datos (Apache Spark está escrito en Scala), en backends concurrentes y distribuidos (Akka, Pekko, ZIO, Cats Effect) y en sistemas financieros y de streaming. Este tutorial usa la sintaxis de Scala 3, que permite bloques basados en sangría; las llaves siguen funcionando.

Instalar Scala y ejecutar Hello, world

El instalador recomendado es Coursier (cs setup), que instala el comando scala y un JDK. En Scala 3, un método anotado con @main es el punto de entrada del programa.

// hello.scala
@main def hello(): Unit =
  println("Hello, world!")
scala run hello.scala
# Hello, world!

Para proyectos más grandes, sbt y Mill son las herramientas de compilación habituales. Scastie (scastie.scala-lang.org) ejecuta Scala en el navegador sin instalar nada.

Valores, variables y tipos básicos

Usa val para un valor inmutable y var para una variable mutable. Los tipos se infieren, pero se pueden escribir después de dos puntos. El Scala idiomático usa val casi siempre.

val name = "Scala"            // String
var count: Int = 1            // mutable
count += 1
val ratio = 3.0 / 4           // Double: 0.75
val big = 12345678901L        // Long
val ok = count > 1            // Boolean
println(s"$name: count=$count ratio=$ratio ok=$ok")
// Scala: count=2 ratio=0.75 ok=true
println(f"pi is ${math.Pi}%.3f")   // pi is 3.142
TipoEjemploNotas
Int42entero de 32 bits con signo
Long42Lentero de 64 bits con signo
Double3.14punto flotante de 64 bits
Booleantruetrue o false
String"hi"cadena de Java; interpoladores s"..." y f"..."
Unit()como void: sin valor significativo

En Scala todo es una expresión: if, los bloques y match devuelven un valor, así que rara vez necesitas vars temporales.

Control de flujo y pattern matching

if devuelve un valor. for recorre rangos y colecciones, y con yield construye una colección nueva. match es el potente pattern matching de Scala, una de las señas de identidad del lenguaje.

val n = 7
val parity = if n % 2 == 0 then "even" else "odd"
println(parity)                        // odd
for i <- 1 to 3 do print(s"$i ")       // 1 2 3
println()
val squares = for i <- 1 to 4 yield i * i
println(squares)                       // Vector(1, 4, 9, 16)
def describe(x: Any): String = x match
  case 0             => "zero"
  case i: Int if i < 0 => "negative int"
  case i: Int        => s"int $i"
  case s: String     => s"string of length ${s.length}"
  case (a, b)        => s"pair $a and $b"
  case _             => "something else"
println(describe(-5))                  // negative int
println(describe("hey"))               // string of length 3
println(describe((1, "x")))            // pair 1 and x

1 to 3 incluye el 3; 1 until 3 se detiene antes del 3. El último case _ es un comodín que coincide con cualquier cosa; sin él, un valor sin coincidencia lanza un MatchError.

Funciones y funciones de orden superior

Los métodos se definen con def. Las funciones son valores: puedes guardarlas, pasarlas como argumentos y devolverlas. Los parámetros pueden tener valores por defecto, y quien llama puede usar argumentos con nombre.

def add(a: Int, b: Int): Int = a + b
def greet(name: String, greeting: String = "Hello"): String =
  s"$greeting, $name!"
val double = (x: Int) => x * 2        // function value
def applyTwice(f: Int => Int, x: Int): Int = f(f(x))
println(add(2, 3))                    // 5
println(greet("Ann"))                 // Hello, Ann!
println(greet("Ann", greeting = "Hi")) // Hi, Ann!
println(applyTwice(double, 5))        // 20
println(List(1, 2, 3, 4).filter(_ % 2 == 0).map(_ * 10)) // List(20, 40)

El guion bajo en _ * 10 es una abreviatura de una lambda de un parámetro, x => x * 10.

Colecciones

Las colecciones estándar son inmutables por defecto: operaciones como map, filter y :+ devuelven una colección nueva y dejan intacta la original. Las versiones mutables están en scala.collection.mutable.

val nums = List(5, 3, 8)
val more = nums :+ 1                  // List(5, 3, 8, 1); nums unchanged
println(more.sorted)                  // List(1, 3, 5, 8)
println(nums.sum)                     // 16
val v = Vector("a", "b", "c")         // fast indexed access
println(v(1))                         // b
val ages = Map("Ann" -> 31, "Bo" -> 25)
println(ages("Ann"))                  // 31
println(ages.get("Zed"))              // None
println(ages.getOrElse("Zed", 0))     // 0
val unique = Set(1, 2, 2, 3)
println(unique.size)                  // 3
val words = List("apple", "avocado", "banana")
println(words.groupBy(_.head)('a'))  // List(apple, avocado)
ColecciónPara qué sirve
Listlista enlazada inmutable; inserción rápida al principio y acceso rápido a head/tail
Vectorsecuencia indexada inmutable; buena opción general por defecto
Mapbúsqueda por clave y valor
Setelementos únicos y pruebas de pertenencia
Arrayarreglo de Java mutable y de tamaño fijo

Case classes, traits y enums

Una case class es un tipo de datos inmutable que obtiene automáticamente igualdad, toString, copy y soporte para pattern matching. Un trait es como una interfaz que además puede contener código. Scala 3 añade enum para tipos de datos algebraicos.

trait Shape:
  def area: Double
case class Circle(r: Double) extends Shape:
  def area: Double = math.Pi * r * r
case class Rect(w: Double, h: Double) extends Shape:
  def area: Double = w * h
enum Color:
  case Red, Green, Blue
@main def shapes(): Unit =
  val all: List[Shape] = List(Circle(1), Rect(2, 3))
  all.foreach(s => println(f"$s area=${s.area}%.2f"))
  // Circle(1.0) area=3.14
  // Rect(2.0,3.0) area=6.00
  val r = Rect(2, 3)
  println(r == Rect(2, 3))            // true
  println(r.copy(h = 10))             // Rect(2.0,10.0)
  println(Color.values.toList)        // List(Red, Green, Blue)

Otras características distintivas son given y using (parámetros contextuales, antes implícitos), los métodos de extensión, los tipos opacos y los tipos unión como Int | String.

Manejo de errores con Option, Either y Try

Scala admite try, catch y finally, pero el código idiomático modela el fallo con tipos. Option es Some(value) o None; Either contiene un error Left o un resultado Right; Try captura una excepción como Failure.

import scala.util.{Try, Success, Failure}
def parseInt(s: String): Option[Int] = s.toIntOption
def safeDivide(a: Int, b: Int): Either[String, Int] =
  if b == 0 then Left("division by zero") else Right(a / b)
println(parseInt("42"))               // Some(42)
println(parseInt("x"))                // None
println(safeDivide(10, 2))            // Right(5)
println(safeDivide(1, 0))             // Left(division by zero)
Try("abc".toInt) match
  case Success(n) => println(s"got $n")
  case Failure(e) => println(s"failed: ${e.getClass.getSimpleName}")
// failed: NumberFormatException

Un programa completo y pequeño: informe de notas

Este programa analiza líneas con el formato name,score, omite las líneas no válidas e imprime el promedio de cada estudiante. Usa case classes, Option, groupBy y formato de cadenas.

case class Score(name: String, value: Int)
def parse(line: String): Option[Score] =
  line.split(",").map(_.trim) match
    case Array(name, v) => v.toIntOption.map(Score(name, _))
    case _              => None
@main def report(): Unit =
  val lines = List("ann, 90", "bo, 70", "ann, 80", "broken line", "bo, x")
  val scores = lines.flatMap(parse)
  val averages = scores.groupBy(_.name).view.mapValues { list =>
    list.map(_.value).sum.toDouble / list.size
  }.toList.sortBy(_._1)
  for (name, avg) <- averages do
    println(f"$name%-4s $avg%.1f")
// ann  85.0
// bo   70.0

Siguientes pasos

  • El Scala 3 Book en docs.scala-lang.org, que cubre el lenguaje desde lo básico hasta las abstracciones contextuales.
  • El Scala Toolkit para tareas cotidianas como archivos, JSON y HTTP.
  • La documentación de Apache Spark si quieres usar Scala para procesar datos.
  • Los cursos enlazados desde scala-lang.org, incluidos los cursos de programación funcional de la EPFL.

Preguntas frecuentes

¿Debo aprender Scala 2 o Scala 3?

Aprende Scala 3 para proyectos nuevos. Scala 2.13 todavía se encuentra en muchos códigos existentes, incluidas partes del ecosistema de Spark, y la mayoría de los conceptos fundamentales son los mismos.

¿Es Scala un lenguaje funcional?

Scala es a la vez orientado a objetos y funcional. Todo valor es un objeto, pero las funciones son valores de primera clase, las colecciones son inmutables por defecto y el pattern matching es central.

¿Cuál es la diferencia entre val y var en Scala?

val define un valor que no se puede reasignar y var define una variable que sí. El Scala idiomático prefiere val junto con colecciones inmutables.

¿Qué es una case class?

Una case class es una clase pensada para datos inmutables. El compilador genera igualdad estructural, un toString legible, un método copy y soporte para pattern matching.

¿Se sigue usando Scala?

Sí. Sigue siendo un lenguaje central para Apache Spark, para plataformas de streaming y datos, y para bibliotecas de backend funcional como ZIO y Cats Effect.

Todos los tutoriales de programación