Scala Generics III: Generalized type constraints

Compartir esta publicación

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:

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…

  Sobre Dioses y Procrastinación: Gestión ágil de proyectos

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.

  Explaining BEMIT: ITCSS + BEM

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!

  Libros de arquitectura de software que se presentarán en GSAS 2023

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 

Author

  • Rafael Ruiz

    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

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Suscríbete a nuestro boletín de noticias

Recibe actualizaciones de los últimos descubrimientos tecnológicos

¿Tienes un proyecto desafiante?

Podemos trabajar juntos

apiumhub software development projects barcelona
Secured By miniOrange