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.
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
new
opcionalEn 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
:
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(...)
Cuidado con los objetos acompañantes
Si defines manualmente un object
con el mismo nombre de la clase (como object Position
), incluso si su método apply
tiene una firma distinta al constructor principal, Scala ya no generará automáticamente el método apply
universal.
En ese caso, deberás usar new
para invocar constructores que no estén cubiertos por tu propia definición de apply
.
class Position(val x: Int, val y: Int)
object Position:
def apply(): Position = new Position(0, 0)
end Position
val pos = Position(x = 541, y = 133) // Esto ya no compila
🧱 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.
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
ovar
, 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:
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:
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.
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
.
Precaución
Los constructores secundarios deben invocar al constructor primario como primera instrucción (ya sea directamente o a través de otro constructor secundario). No se permite ejecutar ningún otro código antes de esa llamada.
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:
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étodosapply
universales. - El constructor primario se define junto a la declaración de la clase, y sus parámetros pueden convertirse en propiedades mediante
val
ovar
. - 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
-
En Scala, un parámetro del constructor primario que no se marca con
val
ovar
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 sinval
ovar
no son accesibles más allá del constructor mismo. ↩