无形:Generic.Aux

Shapeless: Generic.Aux

我正在尝试了解 Generic 的工作原理(以及 TypeClass)。 github wiki 上的示例和文档非常少。是否有详细描述 GenericTypeClass 的规范博客 post/文档页面?

具体来说,这两种方法有什么区别?:

def find1[T](implicit gen: Generic[T]): Generic[T] = gen
def find2[T](implicit gen: Generic[T]): Generic[T] { type Repr = gen.Repr } = gen

给出

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]
}

GenericTypeClass 的实施方式以及它们的作用所涉及的问题非常不同,可能需要单独提出问题,因此我将在这里坚持 Generic

Generic 提供了从 case classes(和可能相似的类型)到异构列表的映射。任何案例 class 都有一个独特的 hlist 表示,但任何给定的 hlist 对应于非常非常多的潜在案例 classes。例如,如果我们有以下情况 classes:

case class Foo(i: Int, s: String)
case class Bar(x: Int, y: String)

GenericFooBar提供的hlist表示是Int :: String :: HNil,这也是(Int, String)和任何其他情况的表示class我们可以按此顺序定义这两种类型。

(作为旁注,LabelledGeneric 允许我们区分 FooBar,因为它在表示中将成员名称包含为类型级字符串。)

我们通常希望能够指定大小写 class 并让 Shapeless 找出(唯一的)泛型表示,并使 Repr 成为类型成员(而不是类型参数)允许我们非常干净地做到这一点。如果 hlist 表示类型是一个类型参数,那么你的 find 方法也必须有一个 Repr 类型参数,这意味着你不能只指定 T 并推断出 Repr

使 Repr 成为类型成员才有意义,因为 Repr 由第一个类型参数唯一确定。想象一个像 Iso[A, B] 这样的类型 class 见证 AB 是同构的。这种类型 class 与 Generic 非常相似,但是 A 并不是唯一的 dermine B——我们不能只问 "what is the type that's isomorphic to A?"——所以它不会使 B 成为一个类型成员是没有用的(尽管如果我们真的想的话我们可以——Iso[A] 只是没有任何意义)。

类型成员的问题是它们很容易被遗忘,一旦消失,就永远消失了。事实上,find1 的 return 类型没有被细化(即不包括类型成员)意味着 Generic 实例 returns 几乎是无用。比如这里的静态类型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 },因此编译器传递的 gen 参数to find1 有我们需要的所有静态信息。问题是我们随后显式将该类型向上转换为普通的旧的未优化的 Generic[Foo],从那时起,编译器不知道该实例的 Repr 是什么。

Scala 的路径相关类型为我们提供了一种方法,可以在不向我们的方法添加另一个类型参数的情况下忘记细化。在你的find2中,编译器静态的知道传入的genRepr,所以当你说return类型是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唯一确定了它的类型成员ReprRepr是类型成员而不是类型参数所以我们不必将它包含在我们所有的类型签名中,路径依赖类型使这成为可能,允许我们跟踪 Repr 即使它不在我们的类型签名中。