Translate

domingo, 20 de abril de 2025

Genéricos en Scala: Covarianza y Contravarianza


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.