Table of Contents
Clases Genéricas en Scala
Clases genéricas, tipos abstractos, type bounds, todos estos conceptos son extraños para desarrolladores que vengan de lenguajes en que el uso de genéricos es inexistente o muy poco utilizado, de manera que vamos a tratar de entender lo básico y profundizar un poco en los type bounds en este primer artículo sobre genéricos.
Vamos a trabajar con este pequeño set de clases e iremos modificando la clase Parking:
trait Thing
trait Vehicle extends Thing
class Car extends Vehicle
class Jeep extends Car
class Coupe extends Car
class Motorcycle extends Vehicle
class Vegetable
class Parking[A](val place: A)
Algo de nomenclatura: si pensamos en Thing[A], es una clase genérica, y A es el tipo abstracto de esta clase.
Antes de empezar tratemos de entender cómo funcionan las clases genéricas y la inferencia de tipos abstractos en Scala.
Parking[A] indica que el valor que le pasemos por parámetro ‘plaza’ debe ser del tipo A, con lo cual si creamos un Parking[Motorcycle], deberemos inicializarlo con una instancia de Motorcycle:
new Parking[Motorcycle](new Motorcycle)
si probamos esto:
new Parking[Motorcycle](new Car)
No compila, ya que Car y Motorcycle no son el mismo tipo, obviamente.
Y que ocurre si probamos con un parking de coches en el que intentamos aparcar un Jeep?
new Parking[Car](new Jeep)
Jeep es subtipo de Car, funciona, claro!
Pero hemos dicho que hablaríamos un poco de la inferencia de tipos, vamos allá. Primero vamos a cambiar un poco a Parking:
class Parking[A](val place1: A, val place2: A)
Si seguimos el ejemplo anterior, un Parking[Car] debería recibir dos cars, dos jeeps, un car y un jeep o algo similar, pero qué ocurre si no explicitamos el tipo?
new Parking(new Car, new Motorcycle)
Esto compila, se le asigna un tipo a A, la inferencia de tipos nos facilita la labor, pero ¿De qué tipo es A ahora que no lo hemos indicado nosotros?
El compilador evaluará el tipo de los dos parámetros, si estos fueran del mismo tipo, la cosa sería fácil, pero al no serlo, busca el supertipo común más próximo.
Una sencilla forma de comprobarlo es crear este Parking en el Scala REPL, la respuesta es clara:
val a = new Parking(new Car, new Motorcycle)
*a: Parking[Vehicle] = Parking@278e1674*
Ha inferido correctamente que Parking debe ser un Parking de Vehicles. Y hasta aquí la introducción, vamos a lo interesante.
Scala type bounds
En el ejemplo anterior no habían restricciones, podíamos crear un Parking[Vegetables] y el mundo seguiría girando, pero en la vida real queremos imponer ciertas reglas al usar clases genéricas.
¿Cómo podemos crear estas restricciones sobre los type parameter? Para crear restricciones sobre cómo de genérico puede ser un tipo podemos hacer uso de los type bounds.
> Upper type bounds
class Parking[A <: Vehicle]
El más sencillo de entender es el upper type bound ‘<:’, este identificador vendría a ser el equivalente a ‘:’ cuando creamos un valor y lo tipamos.
val a: Parking significa que ‘a’ debe ser del tipo Parking o subtipo de Parking, dado de Parking es una clase.
En el caso de los tipos, Parking[A <: Vehicle] significa que el tipo A debe ser del tipo Parking o un subtipo de Parking.
Por ese motivo si creamos un Parking de Vehicle, Car, Jeep o Motorcycle, todo funciona.
Las siguientes lineas podrían ser introducidas en un test y no solo compilaría si no que daría SUCCESS:
new Parking[Vehicle] shouldBe a[Parking[_]]
new Parking[Car] shouldBe a[Parking[_]]
new Parking[Jeep] shouldBe a[Parking[_]]
new Parking[Motorcycle] shouldBe a[Parking[_]]
Pero si intentamos crear un parking de Vegetable o de Thing(superclase de Vehicle) no compilará. Nuestro compilador nos protege.
Si introducimos estas lineas en un test, ni siquiera compilará:
new Parking[Vegetable] should be(a[Parking[_]])
new Parking[Thing] should be(a[Parking[_]])
* _ refers to existential types: https://typelevel.org/blog/2016/01/28/existential-inside.html
> Lower type bounds
Por otra parte tenemos el lower type bound ‘>:’, que indica justo lo contrario a ‘<:’.
[A >: Vehicle] restringiría A a superClases de Vehicle, Vehicle incluído.
Sus usos están mayormente relacionados con la covarianza y contravariancia y pese a que este tema lo trataremos en el segundo capítulo sobre clases genéricas, explicaremos qué es un lower type bound:
Empecemos entendiendo que tipo de relación representa un lower type bound. A >: B significa que A debe ser B o supertipo de B, siendo B la frontera(bound).
class Parking[A >: Jeep](val place: A)
En este Parking podríamos aparcar cualquier superclase de Jeep, es decir, Car, Vehicle, Thing.. AnyRef.. vaya, parece que es demasiado genérica, no?
class Parking[A >: Jeep <: Vehicle](val plaza: A)
Esta es una forma de acotarlo, si pensamos en un árbol de clases de este estilo:
trait Thing
class Vehicle extends Thing
class Car extends Vehicle
class Jeep extends Car
class Coupe extends Car
class Motorcycle extends Vehicle
class Bicycle extends Vehicle
class Tricycle extends Bicycle
Podríamos limitar parking a todos los subtipos de Vehicle, por encima de Tricycle?
class Parking[A >: Bicycle <: Vehicle](val plaza: A)
Parece que sí, vamos a comprobarlo:
new Parking(new AnyRef)
<console>:12: error: inferred type arguments [Object] do not conform to class Parking's type parameter bounds [A >: Bicycle <: Vehicle]
new Parking(new AnyRef)
^
<console>:12: error: type mismatch;
found : Object
required: A
new Parking(new AnyRef)
^
AnyRef da error de compilación, perfecto, vemos que el upper type bound funciona.
new Parking(new Bicycle)
res5: Parking[Bicycle] = Parking@1f3f425b
new Parking(new Vehicle)
res6: Parking[Vehicle] = Parking@61fb801b
new Parking(new Coupe)
res7: Parking[Vehicle] = Parking@e959286
Y llegamos a la preguna estrella, que pasará con Tricycle?
new Parking(new Tricycle)
res8: Parking[Bicycle] = Parking@53c20385
Seguro que muchos no os lo esperabais: funciona.
Sin embargo quien compila no es traidor, y efectivamente el tipo de Parking es Parking[Bicycle], siendo Tricyle un subtipo de Bicycle, la inferencia de tipos ha buscado un supertipo que encajara con las fronteras fijadas y lo ha encontrado, por Liskov allá donde puedas poner una Bicycle puedes poner un Tricycle, y todos contentos.
Sin embargo este no es el uso típico de los lower type bound.
Normalmente se utilizan para poder usar un tipo covariante en una posición contravariante(en el próximo capítulo de Scala Generics hablaré sobre las posicones co/contravariates)
Para que os hagáis una idea, si creas un tipo covariante Thing[+A], hay ciertas reglas que te impiden usarlo en ciertas posiciones, por ejemplo:
class Parking[+A] {
def parkIt(element: A): Parking[A] = new Parking(element)
}
Pese a que esto parece ser correcto no lo es, «element: A» está usando A en posición contravariante pese a que es un tipo covariante.
En serio, no os preocupéis si no entendéis que significa esto, se explicará en otro artículo, solo entended que el compilador os lo prohibe.
Entonces, cómo podemos tener un método que me cree Parkings según el parámetro de entrada? Usando lower type bounds:
class Parking[+A] {
def parkIt[B >: A](element: B): Parking[B] = new Parking(element)
}
Debido a que el lower type bound(también el upper) incluyen la frontera, añadiendo un lower type bound podemos usar el tipo covariante A para generar la frontera del lower type bound sobre B y dejar que sea B quien lo tipe todo. Tachán.
Use-site variance
Otro uso para los type bounds es el Use-site variance. Use-site variance es la forma que se usa en Java para hacer varianza.
El Use-site variance consiste en declarar las fronteras al declarar el tipo, si nuestro Parking fuera invariante sería la única forma de hacer algún tipo de varianza:
class Parking[A](val place: A){
def dosomething(p1: Parking[_ <: Vehicle]) {}
}
Pese a que nuestro Parking no tiene Bounds ni covarianza, aceptaría cualquier tipo como A, en nuestro método dosomething hemos creado una frontera, de esta forma hemos creado un tipo covariante para Parking, ahora podemos hacer esto:
dosomething(new Parking(new Car))
pero no esto:
dosomething(new Parking(new AnyRef))
<console>:12: error: type mismatch;
found : Object
required: Vehicle
dosomething(new Parking(new AnyRef))
^
Pasaría lo mismo si utilizáramos Thing ya que AnyRef es superclase de cualquier tipo que podamos definir.
Si no hicieramos uso del use-site variance, pasarían errores de compilación de este estilo:
def dosomething(p1: Parking[Vehicle]) {}
var p1 = new Parking(new Car)
dosomething(p1)
<console>:14: error: type mismatch;
found : Parking[Car]
required: Parking[Vehicle]
Note: Car <: Vehicle, but class Parking is invariant in type A.
You may wish to define A as +A instead. (SLS 4.5)
dosomething(p1)
^
Ya que solo aceptaría Parking[Vehicle]
Lo último que me gustaría comentar es: hay que tener cuidado con la inferencia de tipos,
al no declarar nosotros los tipos y dejar que sea el compilador quien se encargue, pueden pasar cosas tenebrosas.
Acabamos de ver como dosomething no aceptaba p1 debido a que no era el Parking que esperaba, pero que pasará si hacemos esto:
dosomething(new Parking(new Car))
La inferencia de tipos buscará uno que encaje, subirá el tipo de Car a Vehicle, creando un Parking[Vehicle] y haciendo que todo funcione.
Con estas herramientas ya deberíais ser capaces de entender la mayoría de códigos dónde el uso de clases genéricas sea común y empezar a escribirlo vosotros.
En los siguientes artículos seguiré hablando sobre genéricos, varianza y type constraints. Así como de las razones por las cuales usar genéricos en lugar de polimorfismo.
Si estás intersado en Scala, programación, o en buenas prácticas en el desarrollo de software, Te recomiendo suscribirte a nuestra newsletter mensual para recibir los últimos consejos.
Si este artículo sobre Clases genéricas te gustó, te puede interesar:
Simular respuestas del servidor con Nodejs
Principio de responsabilidad única
Arquitectura de microservicios
F-bound en Scala: traits genéricos con higher-kinded types
Scala generics II: Covarianza y Contravarianza
Suscríbete a nuestro newsletter para estar al día de los eventos, meet ups y demás artículos!
Author
-
Software developer with over 8 years experience working with different code languages, able to work as a FullStack Developer, with the best skills in the backend side. Passionate about new technologies, and the best software development practices involved in a DevOps culture. Enjoying mentoring teams and junior developers and I feel very comfortable with the stakeholders management-wise.
Ver todas las entradas