Skip to main content

Tipos producto como clases en Scala

Los tipos producto son una de las construcciones fundamentales en el diseño de estructuras de datos: permiten agrupar múltiples valores heterogéneos en una única unidad compuesta. En Scala, aunque la forma más idiomática de representarlos suele ser mediante case class, comenzar con clases comunes es útil para comprender con mayor claridad cómo se construyen, se inicializan y se comportan estos tipos.

Esta lección explora cómo representar tipos producto en Scala usando clases regulares, comparando sus características con otras representaciones como tuplas. Se analizan aspectos clave como los constructores primarios y secundarios, la lógica de inicialización y el uso de parámetros por defecto. El objetivo es comprender cómo construir tipos robustos, mantenibles y expresivos, fundamentales para diseñar bibliotecas reutilizables en Scala.

🏗️ Clases comunes como tipos producto en Scala

Una forma directa de representar tipos producto en Scala es mediante clases comunes. Estas clases permiten agrupar múltiples valores con significado semántico bajo una única estructura con nombre.

Posición como tipo producto en Scala (type-fundamentals/algebraic-data-types/product/src/main/scala/cl/ravenhill/Position.scala)
class Position(val x: Int, val y: Int):
def isOrigin: Boolean =
x == 0 && y == 0
end isOrigin
end Position

@main def positionExample(): Unit =
val pos = Position(x = 541, y = 133)
println(pos.x) // Prints: 541
println(pos.y) // Prints: 133
println(pos.isOrigin) // Prints: false
end positionExample

¿Qué acabamos de hacer?

La clase Position es un ejemplo de tipo producto modelado mediante una clase común. A diferencia de Tuple o Pair, esta estructura proporciona nombres explícitos (x, y) para sus componentes, lo que mejora la legibilidad y expresividad del código.

Además, permite incluir comportamiento directamente asociado al dato, como el método isOrigin.

new opcional

En Scala 2, instanciar una clase común requería usar new, a menos que se definiera manualmente un método apply en el objeto acompañante.

Scala 3 introduce los métodos apply universales (también llamados constructor proxies), que permiten instanciar cualquier clase concreta sin necesidad de new, siempre que el objeto acompañante (si existe) no defina un miembro llamado apply.

Esta mejora unifica la sintaxis de creación de instancias entre case class y clases comunes, y contribuye a reducir el "ruido" visual del código.

El mismo comportamiento puede lograrse manualmente en Scala 2 (o en Scala 3 si defines tu propio objeto acompañante) escribiendo un método apply:

Scala 2: Instanciación sin new mediante apply
class Position(val x: Int, val y: Int)

object Position {
def apply(x: Int, y: Int): Position = new Position(x, y)
}

val pos = Position(3, 4) // Llama a Position.apply(...)

🧱 Constructores primarios y secundarios

Al igual que en Kotlin, Scala permite definir constructores primarios y secundarios.

🔹 Constructor primario

En Scala, el constructor primario se define como parte de la declaración de la clase, de forma similar a cómo se hace en Kotlin. Los parámetros declarados en la cabecera de la clase se convierten automáticamente en parte del constructor primario.

Constructor primario en Scala (type-fundamentals/algebraic-data-types/product/src/main/scala/cl/ravenhill/people/Person.scala)
class Person(val name: String, var age: Int)

Al igual que en Kotlin:

  • val crea una propiedad inmutable accesible desde fuera de la clase.
  • var crea una propiedad mutable.
  • Si se omite val o var, el parámetro será solo un argumento del constructor,1 no una propiedad visible desde el exterior.

🔸 Anotaciones y modificadores

Cuando necesitas aplicar una anotación o un modificador de visibilidad al constructor primario, puedes colocarlo directamente antes de la lista de parámetros o del constructor entero:

Anotaciones y modificadores en el constructor primario
class Person @targetName("createPerson") private (val name: String)

¿Qué acabamos de hacer?

  • @targetName("createPerson") cambia el nombre del constructor en el bytecode, lo que puede facilitar la interoperabilidad con Java o evitar conflictos con sobrecargas.
  • private restringe el acceso al constructor, lo que permite controlar la creación de instancias desde un companion object o una función de fábrica.

🔸 Lógica de inicialización

En lugar de usar bloques init como en Kotlin, Scala permite incluir código directamente en el cuerpo de la clase, que se ejecutará como parte del constructor primario:

Lógica de inicialización en el constructor primario (type-fundamentals/algebraic-data-types/product/src/main/scala/cl/ravenhill/people/Person.scala)
class Person(val name: String):
require(name.nonEmpty, "Name cannot be empty")
end Person

¿Qué acabamos de hacer?

Este código se ejecuta inmediatamente al construir el objeto, antes de que se retorne la instancia.
Esto es posible porque el cuerpo de la clase forma parte del constructor primario, lo que permite realizar validaciones o inicializaciones adicionales sin estructuras especiales.

🔹 Constructores secundarios

Scala permite definir constructores secundarios utilizando la palabra clave this. Estos constructores ofrecen formas alternativas de inicializar una clase, pero siempre deben invocar el constructor primario como primera instrucción.

Constructor secundario en Scala (type-fundamentals/algebraic-data-types/product/src/main/scala/cl/ravenhill/people/Person.scala)
class Person(val name: String):
var age: Int = 0

def this(name: String, age: Int) =
this(name)
this.age = age
end this
end Person

¿Qué acabamos de hacer?

En este ejemplo, el constructor secundario permite crear una instancia de Person proporcionando tanto name como age.
Llama al constructor primario con this(name) y luego inicializa la propiedad age.

Alternativa idiomática

En la práctica, es más común usar parámetros por defecto en el constructor primario en lugar de constructores secundarios:

Parámetros por defecto en el constructor primario
class Person(val name: String, var age: Int = 0)

Esta forma es más concisa, clara y más utilizada en código Scala idiomático moderno.

🎯 Conclusiones

En esta lección exploramos cómo representar tipos producto en Scala utilizando clases comunes, una herramienta clave para modelar datos estructurados de forma clara y segura. A través de ejemplos concretos, vimos cómo definir propiedades, incluir lógica de inicialización y aprovechar constructores para crear objetos con distintas configuraciones.

Scala 3 facilita este proceso con mejoras notables, como la eliminación opcional de new al instanciar clases y una sintaxis más concisa para declarar constructores y parámetros. Además, aprendimos a incorporar validaciones directamente en el cuerpo de la clase y a usar valores por defecto como alternativa idiomática a los constructores secundarios.

🔑 Puntos clave

  • Las clases comunes en Scala permiten representar tipos producto de forma expresiva, con nombres significativos y comportamiento asociado.
  • Scala 3 ofrece una sintaxis más limpia, permitiendo instanciación sin new gracias a los métodos apply universales.
  • El constructor primario se define junto a la declaración de la clase, y sus parámetros pueden convertirse en propiedades mediante val o var.
  • Los constructores secundarios, definidos con this, permiten formas alternativas de creación, pero siempre deben invocar primero al constructor primario.
  • Es posible colocar lógica de inicialización en el cuerpo de la clase, lo que habilita validaciones y configuración del estado interno.
  • Los parámetros por defecto son una opción idiomática para simplificar la construcción de objetos sin necesidad de múltiples constructores.

🧰 ¿Qué nos llevamos?

Modelar tipos producto con clases comunes es una práctica fundamental para diseñar librerías reutilizables y expresivas en Scala. Dominar los constructores primarios y secundarios, junto con el uso de lógica de inicialización y parámetros por defecto, permite construir tipos robustos que promueven un código claro, seguro y fácil de evolucionar.

📖 ¿Con ganas de más?

🔥 Referencias recomendadas

  • 🌐 Classes en Tour of Scala:
    Introducción a las clases en Scala, incluyendo su definición, creación de instancias, constructores con valores por defecto, uso de argumentos nombrados, encapsulamiento con miembros privados, y sintaxis de getters/setters. Se destacan las diferencias entre val, var y parámetros sin modificadores en el constructor. Incluye ejemplos en Scala 2 y Scala 3.

🔹 Referencias adicionales

  • 🌐 Universal Apply Methods en Scala 3 reference:
    Este contenido explica cómo Scala 3 extiende la sintaxis simplificada de creación de objetos —anteriormente exclusiva de las *case classes*— a todas las clases concretas, eliminando la necesidad de usar `new`. Se presentan los *constructor proxies*, objetos generados automáticamente que permiten crear instancias mediante llamadas tipo función. El texto detalla las reglas para su generación, sus limitaciones y el objetivo principal: hacer el código más limpio, uniforme y fácil de leer para los desarrolladores.

Footnotes

  1. En Scala, un parámetro del constructor primario que no se marca con val o var no se convierte en una propiedad del objeto, es decir, no es accesible desde fuera de la clase. Sin embargo, sí está disponible dentro del cuerpo de la clase, incluidos sus métodos. Esto se debe a que el cuerpo de la clase en Scala forma parte del constructor, por lo que estos parámetros pueden usarse como variables locales persistentes.
    Esto difiere de Kotlin, donde los parámetros sin val o var no son accesibles más allá del constructor mismo.