Scala es un lenguaje poderoso que combina programación funcional y orientada a objetos. Uno de sus conceptos más sofisticados es el sistema de tipos paramétricos, o genéricos, que permiten escribir código flexible y seguro.
Los genéricos permiten parametrizar clases, traits y métodos con tipos. Por ejemplo:
class Caja[T](valor: T) {
def get: T = valor
}
Esta clase puede contener un Int, un String, o cualquier otro tipo.
Imaginemos que tenemos las siguientes clases:
class Animal
class Perro extends Animal
Ahora, supongamos que existe una clase genérica Caja[T]. ¿Debería Caja[Perro] ser un subtipo de Caja[Animal]?
En Scala, la relación de subtipos entre tipos genéricos no se asume automáticamente. Vos decidís explícitamente cómo se comporta con respecto a la varianza.
Scala permite controlar la varianza de un tipo genérico con anotaciones en la declaración del tipo.
Invarianza (sin anotación):
class Caja[T](valor: T)
No hay relación de subtipos entre Caja[Perro] y Caja[Animal].
Covarianza (+T)
class Caja[+T](valor: T)
Caja[Perro] es subtipo de Caja[Animal] si Perro es subtipo de Animal.
Usá covarianza cuando vas a leer datos del tipo genérico, pero no escribir.
Ejemplo:
class ListaCovariante[+A](val cabeza: A)
No podés definir un método como:
def setCabeza(a: A): Unit // ERROR con +A
Porque podría romper la seguridad de tipos.
Contravarianza (-T)
class Procesador[-T] {
def procesar(t: T): Unit = println("Procesando")
}
Procesador[Animal] es subtipo de Procesador[Perro].
Esto es útil cuando recibís datos del tipo genérico (por ejemplo, funciones o procesadores).
Veamos un ejemplo:
class Animal
class Perro extends Animal
class Gato extends Animal
// Covariante: productor
class CajaProductora[+A](val valor: A)
// Contravariante: consumidor
class Procesador[-A] {
def procesar(a: A): Unit = println(s"Procesando: $a")
}
Uso:
val perro = new Perro
val gato = new Gato
val caja: CajaProductora[Animal] = new CajaProductora[Perro](perro)
val procesador: Procesador[Perro] = new Procesador[Animal]()
procesador.procesar(perro)
En Scala, los tipos de las funciones son contravariantes en los parámetros y covariantes en el resultado.
val f: Perro => Animal = (a: Animal) => a // válido
¿Por qué? Porque si necesitás una función que acepte Perro, es seguro usar una función que acepte Animal, ya que Animal puede incluir a Perro.
La varianza en Scala te permite expresar con precisión las relaciones de subtipos entre clases genéricas:
- +T (covariante): útil para leer
- -T (contravariante): útil para escribir
- T (invariante): comportamiento por defecto
Comprender este sistema es clave para diseñar APIs robustas, especialmente en programación funcional y colecciones.