难以理解无形代码

Hard to understand Shapeless code

我正在努力学习shapeless,但是我发现Shapeless代码真的很难理解。

所以我从 youtube 上的一次演讲中得到了这个代码示例。

https://www.youtube.com/watch?v=JKaCCYZYBWo

任何人都可以向我解释发生了什么(一步一步)。我发现最困难的是一切都是隐式的,所以很难追踪代码....

另外,请给我指出一些可以帮助我理解这样的代码的资源。每次遇到那么多implicit的代码都觉得自己连Scala都不懂

import shapeless._

sealed trait Diff[A]

final case class Identical[A](value: A) extends Diff[A]
final case class Different[A](left : A, right : A) extends Diff[A]

object Diff {
    def apply[A](left : A, right : A) : Diff[A] = {
        if (left == right) Identical(left)
        else Different(left, right)
    }
}

trait SimpleDelta[R <: HList] extends DepFn2[R, R] {
    type Out <: HList
}

object SimpleDelta {

    type Aux[I <: HList, O <: HList] = SimpleDelta[I]{type Out = O}
    implicit def hnilDelta: Aux[HNil, HNil] = new SimpleDelta[HNil] {
        type Out = HNil
        def apply(l : HNil, r: HNil) : Out = HNil
    }
    implicit def hconsDelta[H, T <: HList, DT <: HList](implicit tailDelta: Aux[T, DT])
        : Aux[H::T, Diff[H] :: DT] = new SimpleDelta[H :: T] {
            type Out = Diff[H] :: DT
            def apply(l : H :: T, r: H :: T) : Out = 
                Diff(l.head, r.head) :: tailDelta(l.tail, r.tail)
        }
    def apply[A, R <: HList](l : A, r: A)
        (implicit genA: Generic.Aux[A, R], delta: SimpleDelta[R]) : delta.Out = 
            delta(genA.to(l), genA.to(r))
}

case class Address(number: Int, street: String, city: String)
case class Character(name: String, age: Int, address: Address)
val homer = Character("Homer Simpson", 42, Address(742, "Evergreen Terrace", "SpringField"))
val ned = Character("Ned Flanders", 42, Address(744, "Evergreen Terrace", "SpringField"))

SimpleDelta(homer, ned)
  1. 该程序的目的是比较同一案例的两个实例 class。这是通过异构列表 (HLists) 完成的,它可以作为 case classes 的通用表示。 shapeless 提供了在 case class 和它的通用 HList 表示之间进行转换的基础设施。请注意,在此示例中,差异不是递归计算的,即任何嵌套案例 class 实例将被自动比较。
  2. 让我们假设 Diff 特征和伴随对象是不言自明的。
  3. SimpleDelta 特征定义了一个有两个参数的函数 (DepFn2)。签名是(R, R) => Out。此外,声明了对结果类型的约束:Out <: HList。这意味着对于 SimpleDelta 的所有子类型,类型成员 Out 必须是 HList.
  4. 的子类型
  5. 对象 SimpleDelta 定义类型 Aux。您可以在网上找到有关 Aux 模式的更多信息,例如here。简而言之,它充当 SimpleDelta 特征的别名,允许使用类型参数表达 Out 类型成员。
  6. 现在 SimpleDelta 对象定义了两个隐式方法 hnilDeltahconsDelta。这些方法中的每一个都代表 HListHNilHCons 的可能构造函数之一。 HList A :: B :: HNil 也可以读作 ::(A, ::(B, HNil))HCons(A, HCons(B, HNil))。使用这两种方法,可以对输入HLists进行递归解构和处理。基于这些方法的return类型,编译器在遇到SimpleDelta[R]类型的隐式参数时知道要隐式解析哪个:当R被推断为HNil, 它将调用 hnilDelta;当 R 对于某些类型 HeadTail 被推断为 Head :: Tail(或 HCons(Head, Tail))时,它将调用 hconsDelta.
  7. hnilDelta 方法确定两个空 HList 之间的增量(HNil 是空 HList)。
    1. 注意Out类型成员设置为HNil,这是HNil对象的类型。结果是HNil,因为没有区别。
    2. 方法的return类型是Aux[HNil, HNil]。如上所述,这是 SimpleDelta[HNil] { type Out = HNil } 的别名。它告诉编译器在遇到 SimpleDelta[R] 类型的隐式参数时调用此方法,并且 R 已被推断为 HNil.
  8. hconsDelta 方法确定两个非空 HList 之间的增量,即两个 HListsH 类型的头元素组成, T 类型的尾列表。
    1. 类型参数DT表示HList子类型,表示尾列表的delta。
    2. 注意隐式参数 tailDelta :Aux[T, DT]。它表示该方法必须能够确定两个列表 lr 尾部之间的增量:tailDelta(l.tail, r.tail).
    3. 方法的return类型是Aux[H::T, Diff[H] :: DT]。如上所述,这是 SimpleDelta[H::T] { type Out = Diff[H] :: DT } 的别名。它告诉编译器在遇到 SimpleDelta[R] 类型的隐式参数时调用此方法,对于某些类型 HTR 已被推断为 H::T ].
    4. 类型成员 Out 设置为 Diff[H] :: DT,这是结果差异的 HList 类型。
    5. apply 方法确定两个头元素之间的差异并将其添加到尾列表之间的差异,这是使用隐式参数 tailDelta 计算得出的(见上文)。
  9. apply 方法负责将类型 A 的实例 lr 转换为它们的通用 HList 表示,并调用 SimpleDelta 在这些 HList 上计算差异。
    1. 类型 RA 的通用 HList 表示。隐式参数genA: Generic.Aux[A, R]表示该方法需要在AR之间进行转换。
    2. 此外,该方法声明了一个隐式参数 delta: SimpleDelta[R]。这是计算 lr 的两个通用 HList 表示之间的差异所必需的。
    3. 方法的return类型是delta.Out。请注意,此处使用了依赖类型,因为 delta 参数的实际 Out 类型成员未知 – 它取决于实际类型 A 及其 HList 表示 R.

编译SimpleDelta(homer, ned)时会发生什么?

  1. 键入的调用是 SimpleDelta.apply[Character, R](homer, ned)
  2. 编译器尝试为隐式参数 genA: Generic.Aux[Character, R]delta: SimpleDelta[R].
  3. 寻找匹配值
  4. shapeless 为 case classes A 提供隐式 Generic.Aux[A, R]。这种机制相当复杂,超出了这个问题的范围,但相关的是类型 Character 的通用 HList 表示是 String :: Int :: Address :: HNil,因此编译器可以推断类型genAGeneric.Aux[Character, String :: Int :: Address :: HNil]。这反过来意味着类型变量 R 可以推断为 String :: Int :: Address :: HNil.
  5. 既然类型 R 已知,编译器将尝试解析参数 delta: SimpleDelta[R] 的隐式值,即 SimpleDelta[String :: Int :: Address :: HNil]。当前作用域不包含具有此 return 类型的任何隐式方法或具有此类型的值,但编译器还将查询参数类型 SimpleDelta 的隐式作用域,其中包括伴随对象 SimpleDelta(请参阅 Scala 文档中的 Implicit Parameters 部分)。
  6. 编译器在 SimpleDelta 对象中搜索提供 SimpleDelta[String :: Int :: Address :: HNil] 类型对象的值或方法。唯一匹配的方法是 hconsDelta,因此编译器将参数值 delta 连接到此方法的调用。请注意,HList 被分解为头部和尾部:hconsDelta[String, Int :: Address :: HNil, DT](implicit tailDelta: Aux[Int :: Address :: HNil, DT]).
  7. 填写隐式tailDelta参数,编译器会不断寻找匹配的隐式方法。这样,HList 被递归解构,直到只剩下 HNil 并调用 hnilDelta
  8. 在"way up"上,DT类型的变量会被推断为Diff[Int] :: Diff[Address] :: HNilhconsDelta的return类型会被推断为SimpleDelta[String :: Int :: Address :: HNil] {type Out = Diff[String] :: Diff[Int] :: Diff[Address] :: HNil]}.因此,apply 的 return 类型被推断为 Diff[String] :: Diff[Int] :: Diff[Address] :: HNil.