Diferencia entre constructores de tipos y límites de tipos parametrizados en Scala

CorePress2024-01-24  10

Echa un vistazo al siguiente código:

case class MyTypeConstructor[T[_]: Seq, A](mySeq: T[A]) {
    def map[B](f: A => B): T[B] = mySeq.map(f) // value map is not a member of type parameter T[A]
}

case class MyTypeBounds[T[A] <: Seq[A], A](mySeq: T[A]) {
    def map[B](f: A => B): T[B] = mySeq.map(f) 
}

Lo ideal sería que ambos hicieran lo mismo, simplemente definir un mapa ficticio que llame al método del mapa desde Seq. Sin embargo, el primero no se compila mientras el segundo funciona (en realidad, el segundo tampoco funciona, pero omito cosas por simplicidad).

El error de compilación que recibo es que T[A] no tiene un mapa de miembros, pero me extraña porque el constructor de tipos T debería devolver una Seq (que sí tiene un mapa).

¿Alguien puede explicarme qué es conceptualmente diferente entre estas dos implementaciones?

Lo que quieres es una clase de tipos y puntualmente Functor de gatos.

– Luis Miguel Mejía Suárez

27/03/2021 a las 18:43

1

@LuisMiguelMejíaSuárez jaja te conozco por Reddit. Gracias por responder de nuevo. Sí, sé sobre functores y gatos. En realidad, no me importa este ejemplo, sólo quiero entender la diferencia conceptual entre un constructor de tipos y un tipo enlazado.

- Nicolas Schejtman

27/03/2021 a las 18:58



------------------------------------

T[_]: Seq

Esto no dice "T[_] debería devolver una secuencia como esta". Eso es lo que dice correctamente su segundo ejemplo. Esto dice que "T[_] debe satisfacer un implícito cuyo nombre es Seq". Pero T toma parámetros, por lo que realmente no puede ser parte de un implícito. Básicamente, está intentando hacer

case class MyTypeConstructor[T[_], A](mySeq: T[A])(implicit arg: Seq[T[_]])

Pero Seq[T[_]] no tiene sentido como argumento para una función, en primer lugar porque T toma un parámetro que no se proporciona* y en segundo lugar porque Seq no está diseñado para usarse como implícito.

Podemos ver que se trata de una construcción extraña porque puedes eliminar myMap y seguir apareciendo un error.

// error: type T takes type parameters
case class MyTypeConstructor[T[_]: Seq, A](mySeq: T[A]) {}

*Teóricamente, el compilador podría tratar T[_]: Seq comouna declaración de que se requiere un argumento existencial implícito, pero eso no es lo que hace ahora y sería de utilidad cuestionable, incluso si lo hiciera.

2

"T[_] debe satisfacer un valor implícito cuyo nombre es Seq" ¿Puedes dar más detalles sobre esto?

- Nicolas Schejtman

27/03/2021 a las 21:50

2

@SilvioMayolo En Scala 2, T[_]: F no se expandiría a ev implícito: F[T[_]] sino a ev implícito: F[T]. Scala 3 debería resolver esta confusión sobre el diferente significado del guión bajo _ en el sitio de declaración (parámetro de tipo anónimo) y en el sitio de llamada (tipo existencial adecuado).

-Mario Galic

27/03/2021 a las 23:06



------------------------------------

¿Qué es conceptualmente diferente entre estas dos implementaciones?

Podemos restringir los parámetros de tipo polimórfico utilizando subtipos o enfoque de clase de tipo

scala> case class Subtyping[T[A] <: Seq[A], A](xs: T[A]) {
     |   def map[B](f: A => B) = xs.map(f)
     | }
     | 
     | import scala.collection.BuildFrom
     |
     | case class TypeClassVanilla[T[x] <: IterableOnce[x], A](xs: T[A]) {
     |   def map[B](f: A => B)(implicit bf: BuildFrom[T[A], B, T[B]]): T[B] =
     |     bf.fromSpecific(xs)(xs.iterator.map(f))
     | }
     | 
     | import cats.Functor
     | import cats.syntax.all._
     | 
     | case class TypeClassCats[T[_]: Functor, A](xs: T[A]) {
     |   def map[B](f: A => B): T[B] =
     |     xs.map(f) 
     | }
class Subtyping
import scala.collection.BuildFrom
class TypeClassVanilla
import cats.Functor
import cats.syntax.all._
class TypeClassCats

scala> val xs = List(1, 2, 3)
val xs: List[Int] = List(1, 2, 3)

scala> Subtyping(xs).map(_ + 1)
val res0: Seq[Int] = List(2, 3, 4)

scala> TypeClassCats(xs).map(_ + 1)
val res1: List[Int] = List(2, 3, 4)

scala> TypeClassVanilla(xs).map(_ + 1)
val res2: List[Int] = List(2, 3, 4)

Son diferentes enfoques para lograr lo mismo. Con el enfoque de clases de tipos tal vez no tengamos que preocuparnos tanto por organizar las jerarquías de herencia, lo que a medida que el sistema crece en complejidad, podría llevarnos a comenzar a forzar artificialmente las cosas dentro de la jerarquía.

3

Gracias por tu respuesta Mario. Una cosa que estoy viendo es por qué asociamos la sintaxis T[] con clases de tipos, siendo la primera una característica del lenguaje Scala y la segunda un concepto de programación funcional independiente del lenguaje. Qué es¿La diferencia entre el ejemplo de gatos donde T[]: Functor y mi ejemplo donde T[_]: Seq?

- Nicolas Schejtman

28/03/2021 a las 13:22

@NicolasSchejtman Un concepto clave que hay que entender es la diferencia entre el tipo adecuado y el constructor de tipos. La secuencia se declara algo así como rasgo Seq[A], lo que significa que el argumento de tipo A debe ser un tipo adecuado, mientras que el functor se declara algo así como rasgo Functor[F[_]], lo que significa que el argumento de tipo F debe ser un constructor de tipos. Por lo tanto, en el nivel sintáctico T[_]: Seq no puede funcionar porque el constructor de tipos Seq espera un tipo adecuado, pero yoEstás intentando proporcionar un constructor de tipos. En el nivel semántico T[_]: Seq no tiene sentido porque Seq no está diseñado para ser una clase de tipo.

-Mario Galic

28/03/2021 a las 13:48

¡Genial, eso responde a mi pregunta! Muchas gracias Mario 👏

- Nicolas Schejtman

28/03/2021 a las 14:41

Su guía para un futuro mejor - libreflare
Su guía para un futuro mejor - libreflare