I'm trying to understand how Generic
works (and TypeClass
too). The github wiki is very sparse on examples and documentation. Is there a canonical blog post / documentation page describing Generic
and TypeClass
in detail?
In concrete, what is the difference between these two methods?:
def find1[T](implicit gen: Generic[T]): Generic[T] = gen
def find2[T](implicit gen: Generic[T]): Generic[T] { type Repr = gen.Repr } = gen
given
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]
}
The issues involved in how Generic
and TypeClass
are implemented and what they do are different enough that they probably deserve separate questions, so I'll stick to Generic
here.
Generic
provides a mapping from case classes (and potentially similar types) to heterogeneous lists. Any case class has a unique hlist representation, but any given hlist corresponds to a very, very large number of potential case classes. For example, if we have the following case classes:
case class Foo(i: Int, s: String)
case class Bar(x: Int, y: String)
The hlist representation provided by Generic
for both Foo
and Bar
is Int :: String :: HNil
, which is also the representation for (Int, String)
and any other case classes we could define with these two types in this order.
(As a side note, LabelledGeneric
allows us to distinguish between Foo
and Bar
, since it includes the member names in the representation as type-level strings.)
We generally want to be able to specify the case class and let Shapeless figure out the (unique) generic representation, and making Repr
a type member (instead of a type parameter) allows us to do this pretty cleanly. If the hlist representation type were a type parameter, then your find
methods would have to have a Repr
type parameter as well, which means that you wouldn't be able to specify only the T
and have the Repr
inferred.
Making Repr
a type member makes sense only because the Repr
is uniquely determined by the first type parameter. Imagine a type class like Iso[A, B]
that witnesses that A
and B
are isomorphic. This type class is very similar to Generic
, but A
doesn't uniquely dermine B
—we can't just ask "what is the type that's isomorphic to A
?"—so it wouldn't be useful to make B
a type member (although we could if we really wanted to—Iso[A]
just wouldn't really mean anything).
The problem with type members is that they're easy to forget, and once they're gone, they're gone forever. The fact that the return type of your find1
isn't refined (i.e. doesn't include the type member) means that the Generic
instance it returns is pretty much useless. For example, the static type of res0
here might as well be 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
^
When Shapeless's Generic.materialize
macro creates the Generic[Foo]
instance we're asking for, it's statically typed as a Generic[Foo] { type Repr = Int :: String :: HNil }
, so the gen
argument that the compiler hands to find1
has all the static information we need. The problem is that we then explicitly up-cast that type to a plain old unrefined Generic[Foo]
, and from that point on the compiler doesn't know what the Repr
is for that instance.
Scala's path-dependent types give us a way not to forget the refinement without adding another type parameter to our method. In your find2
, the compiler statically knows the Repr
for the incoming gen
, so when you say that the return type is Generic[T] { type Repr = gen.Repr }
, it will be able to keep track of that information:
scala> find2[Foo].to(Foo(1, "ABC"))
res2: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 1 :: ABC :: HNil
scala> res2.head
res3: Int = 1
To sum up: Generic
has a type parameter T
that uniquely determines its type member Repr
, Repr
is a type member instead of a type parameter so that we don't have to include it in all of our type signatures, and path-dependent types make this possible, allowing us to keep track of Repr
even though it's not in our type signatures.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With