En este tercer artículo sobre genéricos en Scala vamos a hablar de más constraints, ahora vamos a por los generalized type constraints:

En Scala Generics I hablamos de las type bounds y use site variance y cómo estos nos proporcionan algo de control sobre nuestros tipos abstractos, sin embargo hay métodos que necesitan asegurarse que el tipo abstracto de la clase cumple unas restricciones concretas solamente en ese método.

De nuevo, vamos a trabajar con este pequeño set de clases para empezar:

trait Thing
trait Vehicle extends Thing
class Car extends Vehicle
class Jeep extends Car
class Coupe extends Car

class Motorcycle extends Vehicle

class Vegetable

Trabajaremos con Parking, como de costumbre.

class Parking[A <: Vehicle](val v: A){
  def park: A = v
}

En este ejemplo el método aparca puede devolver cualquier tipo de vehículo, tal y como el upper type bound de Parking especifica, pero que ocurre si queremos tener lógica específica para aparcar coches y motos, tal que así?

class Parking[A <: Vehicle](val v: A) {
  def aparcaMoto(): A = {
    println("moto") // podría ser llamar a algún método público de Motorcycle
    v
  }
  def aparcaCoche(): A = {
    println("coche") // podría ser llamar a algún método público de Car
    v
  }
}

En estos casos, querremos asegurar que A es de tipo Motorcycle para aparcaMoto y de tipo Car para aparcaCoche, cierto?

Si recordamos algo Generics I veremos que había dos formas de hacer cosas similares, una era con type bounds sobre el método y la otra era con use site variance.

probemos con type bounds:

vamos a intentar añadir bounds a A para los métodos aparcaMoto y aparcaCoche:

class Parking[A <: Vehicle](val v: A) {
  def aparcaMoto[A <: Motorcycle] = {
    println("moto") // podría ser llamar a algún método público de Motorcycle
    v
  }
  def aparcaCoche[A <: Car] = {
    println("coche") // podría ser llamar a algún método público de Car
    v
  }
}

Si esto lo ponéis en un IDE este ya os dará pistas… Suspicious shadowing… Pero da igual, esto compila y lo vamos a probar!

val p1 = new Parking(new Motorcycle)
  p1: Parking[Motorcycle] = Parking@193f604a

p1.aparcaCoche
  res5: Motorcycle = Motorcycle@5562c41e

p1.aparcaMoto
  res6: Motorcycle = Motorcycle@5562c41e  

Parece que esos type bounds no han hecho nada. Obviamente, la pista que nos daba el IDE: Suspicious shadowing by a time parameter. significa que estamos redefiniendo un type parameter.

Resulta que no estabamos añadiendo bounds sobre nuestro A, sino definiendo un nuevo tipo A…

Y si tipamos el retorno del método para asegurarnos?

class Parking[A <: Vehicle](val v: A) {
  def aparcaMoto[B <: Motorcycle]: B = {
    println("moto") // podría ser llamar a algún método público de Motorcycle
    v
  }
}
  <console>:13: error: type mismatch;
      found   : Parking.this.v.type (with underlying type A)
      required: B
                v
                ^            

No es suficiente con eso, ya que estamos añadiendo restricciones sobre el tipo de vuelta.. pero es que queremos trabajar con v: A

es decir, las restricciones no deben ir sobre un nuevo tipo B, sino sobre el tipo A definido en la clase, parece que el type bound no nos sirve…

vamos a probar con use site variance, que si lo recordamos, nos permitía definir las constraints de un tipo genérico en el momento de definirlo:

class Parking[A](val v: A) {}

def aparcaMoto(parking: Parking[_ <: Motorcycle]) = {
  println("moto") // podría ser llamar a algún método público de Motorcycle
  parking.v
}
def aparcaCoche(parking: Parking[_ <: Car]) = {
  println("coche") // podría ser llamar a algún método público de Car
  parking.v
}

Tiene buena pinta, vamos a ver que tal va:

aparcaCoche(new Parking(new Car))
  res1: Car = Car@17baae6e

aparcaCoche(new Parking(new Motorcycle))
  <console>:14: error: type mismatch;
    found   : Motorcycle
    required: Car
    parkCar(new Parking(new Motorcycle))
                            ^    

Parece que esta si puede ser una solución, aunque hemos tenido que sacrificar varias cosas… los métodos aparcaMoto y aparcaCoche los usamos

fuera de Parking, pasándoles un parking como parámetro.. Además, el open-close principle se ha roto al llamar a parking.v (tell don’t ask).

Pese a funcionar, es una solución muy pobre a nuestra problemática.

Y aquí es dónde entran en juego los Generalized type constraints:

Las tres generalized type constraints existentes son =:=, <:< y <%<. Se usan mediante parámetros implicitos (implicit ev: T =:= B) en los método.

Estos parámetros implicitos, generalmente llamodos ev(“evidences”) son pruebas, que demuestran que un tipo cumple ciertas restricciones.

estas constraints se pueden usar de diversas formas, pero lo más interesante es que nos permiten delimitar los type parámeter de la clase en el própio método:

class Parking[A <: Vehicle](val v: A) {
  def aparcaMoto(implicit ev: A =:= Motorcycle) { println("moto") }
  def aparcaCoche(implicit ev: A =:= coche) { println("car")}
}

Mediante el uso de =:= hemos conseguido que un tipo abstracto como Parking, obligue a que su type parameter sea uno concreto para diferentes métodos.

Y qué pasará si yo creo un Parking[Car] y llamo a aparcaMoto, esto petará, no? Pues sí:

val p1 = new Parking(new Car)
  p1: Parking[Car] = Parking@5669f5b9

p1.aparcaCoche

p1.aparcaMoto
  <console>:14: error: Cannot prove that Car =:= Motorcycle.
    p1.aparcaMoto
      ^   

Efectivamente, hemos conseguido tener métodos que solo funcionan cuando el type parameter cumple ciertas restricciones.

Principalmente los generalized type constraints sirven para asegurar que un método específico tiene una constraint concreta, de manera que ciertos métodos podrán usarse con un tipo y otros métodos con otro.
Sin embargo, debido al type erasure, no podemos sobrecargar un método:

class Parking[A <: Vehicle](val vehicle: A) {
  def aparca(implicit ev: A =:= Motorcycle) { println("moto") }
  def aparca(implicit ev: A =:= Car) { println("coche") }
}
  method aparca:(implicit ev: =:=[A,Car])Unit and
  method aparca:(implicit ev: =:=[A,Motorcycle])Unit at line 12
  have same type after erasure: (ev: =:=)Unit
             def aparca(implicit ev: A =:= Car) {} 

Otro caso de uso curioso podría ser el sigiente: Quiero un método para aparcar dos vehiculos iguales.

class Parking[A <: Vehicle](val vehicle: A) {
  def aparca2(vehicle1: A, vehicle2: A) {}
}

Como ya habréis deducido esto no es suficiente, ya que los dos vehiculos podran ser cualquier subtipo de Vehicle, y si el parking que estamos creando es new Parking(new Car), podríamos aparcar un Jeep y un Coupe a la vez.

La solución es un generalized type constraint:

class Parking[A <: Vehicle] {
  def aparca2[B, C](vehicle1: B, vehicle2: C)(implicit ev: B =:= C) {}
}

Ahora vamos a hacer las pruebas:

val p2 = new Parking[Car]
  a: Parking[Car] = Parking@57a68215

p2.aparca2(new Jeep, new Jeep)

p2.aparca2(new Jeep, new Coupe)
  <console>:15: error: Cannot prove that Jeep =:= Coupe.
  a.park2(new Jeep, new Coupe)
              ^                  

Ahora vehicle1 debe ser del mismo tipo que vehicle2, tal que como comprobamos, sin embargo…

p2.aparca2(new Vegetable, new Vegetable)

Vaya.. hemos perdido la constraint de Vehicle. Vamos a arreglarlo! Type bounds al rescate!

class Parking[A <: Vehicle] {
  def aparca2[B <: A, C](vehicle1: B, vehicle2: C)(implicit ev: B =:= C) {}
}

val p3 = new Parking[Car]
p3.park2(new Vegetable, new Vegetable)
  <console>:14: error: inferred type arguments [Vegetable,Vegetable] do not conform to method park2's type parameter bounds [B <: Car,C]
      p3.aparca2(new Vegetable, new Vegetable)
      ^
    <console>:14: error: type mismatch;
          found   : Vegetable
          required: B
          p3.aparca2(new Vegetable, new Vegetable)
                    ^
    <console>:14: error: type mismatch;
          found   : Vegetable
          required: C
          p3.aparca2(new Vegetable, new Vegetable)
                                   ^
    <console>:14: error: Cannot prove that B =:= C.
          p3.aparca2(new Vegetable, new Vegetable)*/

p3.aparca2(new Jeep, new Coupe)
  <console>:15: error: Cannot prove that Jeep =:= Coupe.
         p3.aparca2(new Jeep, new Coupe)
                     ^                            

p3.aparca2(new Jeep, new Jeep)

Ahora el método aparca2 solo puede recibir dos tipos iguales que además deben ser A o subtipo de A, arreglado!

Finalmente, vamos a explicar los otros dos generalized type constraints:

A <:< B, como ya adivinaréis, significa “A debe ser subtipo de B. Es el análogo al type bound <:

Su uso es exactamente el mismo que con =:=, con un implicito “evidence”.

En el caso anterior de aparcaCoche y aparcaMoto, si quisieramos no solo aparcar coches y motos si no subtipos de coches y motos, en lugar de usar =:=, usaríamos <:< :

class Parking[A <: Vehicle](val v: A) {
  def aparcaMoto(implicit ev: A <:< Motorcycle) { println("moto") }
  def aparcaCoche(implicit ev: A <:< Car) { println("car")}
}

Y por supuesto, en el caso de recibir dos type parameter, podemos forzar que uno deba ser subtipo del otro:

class Parking[A <: Vehicle] {
  def aparca2[B <: A, C](vehicle1: B, vehicle2: C)(implicit ev: B <:< C) {}
}

El último (y deprecado) generalized type constraints es <%<, está deprecado y no se encuentra en uso en la stdlib de Scala. Hace referencia al concepto de “view”, también deprecado.

significa que en A <%< B A debe poder ser visto como B. Esto se puede dar mediante una implicit conversion, por ejemplo.

Para aquellos que queráis más información sobre generalized type constraints, saber como funcionan internamente, más comparativas con type bounds etc etc, recomiendo encarecidamente leer este post: http://blog.bruchez.name/2015/11/generalized-type-constraints-in-scala.html

Estos tres articulos sobre genéricos en scala han servido como introducción a los genéricos así como para presentar algunas piezas más complejas, hemos llegado a niveles bastante profundos y eso que solo hemos rascado la superficie, ni siquiera hemos entrado en cuales son sus implementaciones!!

 

Si este artículo sobre generalized type constraints te gustó, te puede interesar: 

La Deuda Técnica 

Simular respuestas del servidor con Nodejs

Principio de responsabilidad única 

Por qué Kotlin ?

Patrón MVP en iOS

Arquitectura de microservicios  

F-bound en Scala: traits genéricos con higher-kinded types

Scala Generics I : Clases genéricas y Type bounds

Scala Generics II: covarianza y contravarianza