scala - Shapeless: Generic.Aux
generics (1)
Las cuestiones relacionadas con la forma en que se implementan Generic
y TypeClass
y lo que hacen son tan diferentes que probablemente merecen preguntas separadas, por lo que me quedo con Generic
aquí.
Generic
proporciona una asignación de clases de casos (y tipos potencialmente similares) a listas heterogéneas. Cualquier clase de caso tiene una representación hlist única, pero cualquier hlist dada corresponde a un número muy, muy grande de clases de casos potenciales. Por ejemplo, si tenemos las siguientes clases de casos:
case class Foo(i: Int, s: String)
case class Bar(x: Int, y: String)
La representación hlist proporcionada por Generic
para Foo
y Bar
es Int :: String :: HNil
, que también es la representación para (Int, String)
y cualquier otra clase de caso que podamos definir con estos dos tipos en este orden.
(Como nota al LabelledGeneric
, LabelledGeneric
nos permite distinguir entre Foo
y Bar
, ya que incluye los nombres de los miembros en la representación como cadenas de nivel de tipo).
Por lo general, queremos poder especificar la clase de caso y dejar que Shapeless resuelva la representación genérica (única), y hacer que Repr
un miembro de tipo (en lugar de un parámetro de tipo) nos permite hacerlo de manera bastante limpia. Si el tipo de representación hlist fuera un parámetro de tipo, entonces sus métodos de find
tendrían que tener un parámetro de tipo Repr
, lo que significa que no podría especificar solo la T
y tener la Repr
inferida.
Convertir a Repr
un miembro de tipo solo tiene sentido porque el Repr
está determinado de forma única por el primer parámetro de tipo. Imagina una clase de tipos como Iso[A, B]
que es testigo de que A
y B
son isomorfos. Esta clase de tipo es muy parecida a la Generic
, pero A
no es la única dermina B
, no podemos simplemente preguntar "¿cuál es el tipo que es isomorfo para A
?", Por lo que no sería útil hacer de B
un miembro de tipo ( aunque podríamos si realmente quisiéramos: Iso[A]
simplemente no significaría nada).
El problema con los miembros de tipo es que son fáciles de olvidar, y una vez que se han ido, se han ido para siempre. El hecho de que el tipo de retorno de su find1
no sea refinado (es decir, no incluye el miembro de tipo) significa que la instancia Generic
que devuelve es bastante inútil. Por ejemplo, el tipo estático de res0
aquí también podría ser Any
:
scala> import shapeless._
import shapeless._
scala> def find1[T](implicit gen: Generic[T]): Generic[T] = gen
find1: [T](implicit gen: shapeless.Generic[T])shapeless.Generic[T]
scala> case class Foo(i: Int, s: String)
defined class Foo
scala> find1[Foo].to(Foo(1, "ABC"))
res0: shapeless.Generic[Foo]#Repr = 1 :: ABC :: HNil
scala> res0.head
<console>:15: error: value head is not a member of shapeless.Generic[Foo]#Repr
res0.head
^
Cuando la macro Generic.materialize de Generic.materialize
crea la instancia Generic[Foo]
que estamos solicitando, se escribe estáticamente como un Generic[Foo] { type Repr = Int :: String :: HNil }
, por lo que el argumento gen
que el compilador entrega find1
tiene toda la información estática que necesitamos. El problema es que luego, de forma explícita, enviamos ese tipo a un Generic[Foo]
sin refinar, y desde ese momento en el compilador no sabemos qué es el Repr
para esa instancia.
Los tipos dependientes de la ruta de Scala nos dan una manera de no olvidar el refinamiento sin agregar otro parámetro de tipo a nuestro método. En su find2
, el compilador conoce estáticamente el Repr
para el gen
entrante, por lo que cuando dice que el tipo de retorno es Generic[T] { type Repr = gen.Repr }
, podrá realizar un seguimiento de esa información:
scala> find2[Foo].to(Foo(1, "ABC"))
res2: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 1 :: ABC :: HNil
scala> res2.head
res3: Int = 1
Para resumir: Generic
tiene un parámetro de tipo T
que determina de forma única su miembro de tipo Repr
, Repr
es un miembro de tipo en lugar de un parámetro de tipo para que no tengamos que incluirlo en todas nuestras firmas de tipo y tipos dependientes de la ruta haga esto posible, permitiéndonos realizar un seguimiento de Repr
aunque no esté en nuestras firmas de tipo.
Estoy tratando de entender cómo funciona Generic
(y TypeClass
también). El wiki de github es muy escaso en ejemplos y documentación. ¿Hay una página de publicación / documentación canónica que describa Generic
y TypeClass
en detalle?
En concreto, ¿cuál es la diferencia entre estos dos métodos ?:
def find1[T](implicit gen: Generic[T]): Generic[T] = gen
def find2[T](implicit gen: Generic[T]): Generic[T] { type Repr = gen.Repr } = gen
dado
object Generic {
type Aux[T, Repr0] = Generic[T] { type Repr = Repr0 }
def apply[T](implicit gen: Generic[T]): Aux[T, gen.Repr] = gen
implicit def materialize[T, R]: Aux[T, R] = macro GenericMacros.materialize[T, R]
}