Generic 和 TypeClass 的实现方式所涉及的问题以及它们的作用有很大不同,因此它们可能需要单独的问题,所以我将在这里坚持使用 Generic。
Generic 提供从案例类(以及可能相似的类型)到异构列表的映射。任何案例类都有唯一的 hlist 表示,但任何给定的 hlist 对应于非常非常多的潜在案例类。例如,如果我们有以下案例类:
case class Foo(i: Int, s: String)
case class Bar(x: Int, y: String)
Generic 为Foo 和Bar 提供的hlist 表示是Int :: String :: HNil,这也是(Int, String) 以及我们可以按此顺序使用这两种类型定义的任何其他案例类的表示。
(附带说明,LabelledGeneric 允许我们区分 Foo 和 Bar,因为它在表示中包含成员名称作为类型级字符串。)
我们通常希望能够指定 case 类并让 Shapeless 找出(唯一的)泛型表示,并且使 Repr 成为类型成员(而不是类型参数)允许我们非常干净地做到这一点。如果 hlist 表示类型是类型参数,那么您的 find 方法也必须具有 Repr 类型参数,这意味着您将无法仅指定 T 并拥有 @987654338 @推断。
将Repr 设为类型成员才有意义,因为Repr 由第一个类型参数唯一确定。想象一个像Iso[A, B] 这样的类型类,它见证了A 和B 是同构的。这个类型类与Generic 非常相似,但A 并不唯一地定义B——我们不能只问“与A 同构的 类型是什么?” ——所以让B 成为类型成员是没有用的(尽管如果我们真的想这样做——Iso[A] 只是没有任何意义)。
类型成员的问题在于它们很容易被遗忘,而且一旦消失,它们就永远消失了。您的 find1 的返回类型没有经过优化(即不包括 type 成员)这一事实意味着它返回的 Generic 实例几乎没有用。比如这里res0的静态类型也可以是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
^
当Shapeless 的Generic.materialize 宏创建我们要求的Generic[Foo] 实例时,它被静态类型化为Generic[Foo] { type Repr = Int :: String :: HNil },因此编译器交给find1 的gen 参数包含我们所有的静态信息需要。问题是我们随后将该类型显式向上转换为一个普通的旧的未精炼的Generic[Foo],并且从那时起编译器不知道该实例的Repr 是什么。
Scala 的路径相关类型为我们提供了一种方法,让我们不要忘记在我们的方法中添加另一个类型参数 的细化。在您的find2 中,编译器静态知道传入gen 的Repr,因此当您说返回类型为Generic[T] { type Repr = gen.Repr } 时,它将能够跟踪该信息:
scala> find2[Foo].to(Foo(1, "ABC"))
res2: shapeless.::[Int,shapeless.::[String,shapeless.HNil]] = 1 :: ABC :: HNil
scala> res2.head
res3: Int = 1
总结一下:Generic有一个类型参数T唯一确定它的类型成员Repr,Repr是一个类型成员而不是一个类型参数,这样我们就不用把它包含在里面了我们所有的类型签名和依赖于路径的类型都使这成为可能,使我们能够跟踪Repr,即使它不在我们的类型签名中。