Informeless: Générique.Aux
J'essaie de comprendre comment fonctionne Generic
(et TypeClass
aussi). Le wiki github est très clairsemé sur les exemples et la documentation. Y a-t-il une page de blog / documentation canonique décrivant Generic
et TypeClass
en détail?
Concrètement, Quelle est la différence entre ces deux méthodes?:
def find1[T](implicit gen: Generic[T]): Generic[T] = gen
def find2[T](implicit gen: Generic[T]): Generic[T] { type Repr = gen.Repr } = gen
Donné
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]
}
1 réponses
Les problèmes impliqués dans la façon dont Generic
et TypeClass
sont implémentés et ce qu'ils font sont assez différents pour qu'ils méritent probablement des questions séparées, donc je vais m'en tenir à Generic
ici.
Generic
fournit un mappage des classes de cas (et des types potentiellement similaires) vers des listes hétérogènes. Toute classe de cas a une représentation hlist unique, mais toute hlist donnée correspond à un très, très grand nombre de classes de cas potentielles. Par exemple, si nous avons le cas suivant classes:
case class Foo(i: Int, s: String)
case class Bar(x: Int, y: String)
La représentation hlist fournie par Generic
pour Foo
et Bar
est Int :: String :: HNil
, qui est également la représentation pour (Int, String)
et toutes les autres classes de cas que nous pourrions définir avec ces deux types dans cet ordre.
(en note de côté, LabelledGeneric
nous permet de faire la distinction entre Foo
et Bar
, car il inclut les noms de membres dans la représentation en tant que chaînes de niveau type.)
Nous voulons généralement être en mesure de spécifier la classe de cas et laisser Informeless comprendre la représentation générique (unique), et faire Repr
un membre de type (au lieu d'un paramètre de type) nous permet de le faire assez proprement. Si le type de représentation hlist était un paramètre de type, vos méthodes find
devraient également avoir un paramètre de type Repr
, ce qui signifie que vous ne pourriez pas spécifier uniquement le T
et avoir le Repr
déduit.
Faire de Repr
un membre de type n'a de sens que parce que Repr
est déterminé de manière unique par le premier paramètre de type. Imaginez une classe de type comme {[22] } qui témoigne que A
et B
sont isomorphes. Cette classe de type est très similaire à Generic
, mais A
ne détermine pas uniquement B
-nous ne pouvons pas simplement demander " quel est le type isomorphe à A
?"-il ne serait donc pas utile de faire de B
un membre de type (bien que nous le puissions si nous le voulions vraiment-Iso[A]
ne signifierait rien).
Le problème avec les membres de type est qu'ils sont faciles à oublier, et une fois partis, ils sont disparu à jamais. Le fait que le type de retour de votre find1
n'est pas raffiné (c'est-à-dire n'inclut pas le membre de type) signifie que l'instance Generic
qu'il renvoie est à peu près inutile. Par exemple, le type statique de res0
ici pourrait aussi bien être 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
^
Lorsque la macro Generic.materialize
de Shapeless crée l'instance Generic[Foo]
que nous demandons, elle est typée statiquement comme Generic[Foo] { type Repr = Int :: String :: HNil }
, donc l'argument gen
que le compilateur remet à find1
a toutes les informations statiques dont nous avons besoin. Le problème est que nous ensuite, convertissez explicitement ce type en un ancien Generic[Foo]
non raffiné, et à partir de ce moment-là, le compilateur ne sait pas ce que Repr
est pour cette instance.
Les types dépendants du chemin de Scala nous permettent de ne pas oublier le raffinement sans ajouter un autre paramètre de type à notre méthode. Dans votre find2
, le compilateur connaît statiquement le Repr
pour le gen
entrant, donc quand vous dites que le type de retour est Generic[T] { type Repr = gen.Repr }
, Il sera capable de garder une trace de cela informations:
scala> find2[Foo].to(Foo(1, "ABC"))
res2: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 1 :: ABC :: HNil
scala> res2.head
res3: Int = 1
Pour résumer: Generic
a un paramètre de type {[18] } qui détermine de manière unique son membre de type Repr
, Repr
est un membre de type au lieu d'un paramètre de type afin que nous n'ayons pas à l'inclure dans toutes nos signatures de type, et les types dépendants du chemin rendent cela possible, nous permettant de garder une trace de Repr
même si ce n'est pas dans nos signatures de type.