我正在尝试学习无形,但是我发现无形代码真的很难理解。
所以我从YouTube上的一次演讲中得到了这个代码示例。
https://www.youtube.com/watch?v=JKaCCYZYBWo
谁能向我解释发生了什么(一步一步)。我觉得最困难的是一切都是隐式的,所以很难跟踪代码....
另外,请指出一些可以帮助我理解此类代码的资源。每次我遇到包含如此多隐式的代码时,我都觉得我什至不了解 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)
- 该程序的目的是比较同一案例类的两个实例。这是通过异构列表(
HList
s)完成的,异构列表可以作为案例类的泛型表示。Shapeless 提供了在案例类及其泛型HList
表示形式之间进行转换的基础结构。请注意,在此示例中,差值不是递归计算的,即任何嵌套的案例类实例都将以原子方式进行比较。 - 让我们假设
Diff
特征和伴随对象是不言自明的。 SimpleDelta
特征定义了一个具有两个参数(DepFn2
)的函数。签名(R, R) => Out
。此外,还声明了对结果类型的约束:Out <: HList
。这意味着对于SimpleDelta
的所有子类型,类型成员Out
必须是HList
的子类型。- 对象
SimpleDelta
定义类型Aux
。您可以在网络上找到有关 AUX 模式的更多信息,例如 这里.简而言之,它充当SimpleDelta
特征的别名,允许使用类型参数表示Out
类型成员。 - 现在
SimpleDelta
对象定义了两个隐式方法hnilDelta
和hconsDelta
。这些方法中的每一个都代表了HList
、HNil
和HCons
的可能构造之一。HList
A :: B :: HNil
也可以读作::(A, ::(B, HNil))
或HCons(A, HCons(B, HNil))
。使用这两种方法,可以递归解构和处理输入HList
s。基于这些方法的返回类型,编译器知道在遇到SimpleDelta[R]
类型的隐式参数时要隐式解析哪一个: 当R
被推断为HNil
时,它将调用hnilDelta
;当R
被推断为某些类型的Head
和Tail
Head :: Tail
(或HCons(Head, Tail)
)时,它将调用hconsDelta
。 hnilDelta
方法确定两个空HList
s 之间的增量(HNil
是空HList
)。- 请注意,
Out
类型成员设置为HNil
,这是HNil
对象的类型。结果是HNil
,因为没有区别。 - 该方法的返回类型为
Aux[HNil, HNil]
。如上所述,这是SimpleDelta[HNil] { type Out = HNil }
的别名。它告诉编译器在遇到SimpleDelta[R]
类型的隐式参数时调用此方法,并且R
已被推断为HNil
。
- 请注意,
hconsDelta
方法确定两个非空HList
s.之间的增量,即由类型为H
的头元素和类型T
的尾表组成的两个HLists
。- 类型参数
DT
表示表示尾部列表增量的HList
子类型。 - 请注意隐式参数
tailDelta :Aux[T, DT]
。它传达了该方法必须能够确定两个列表l
和r
的尾部之间的增量:tailDelta(l.tail, r.tail)
。 - 该方法的返回类型为
Aux[H::T, Diff[H] :: DT]
。如上所述,这是SimpleDelta[H::T] { type Out = Diff[H] :: DT }
的别名。它告诉编译器在遇到类型为SimpleDelta[R]
的隐式参数时调用此方法,并且R
已被推断为对某些类型的H
和T
H::T
。 - 类型成员
Out
设置为Diff[H] :: DT
,这是结果差异的HList
类型。 apply
方法确定两个头元素之间的差异,并将其附加到尾列表之间的差异之前,这是使用隐式参数tailDelta
计算的(见上文)。
- 类型参数
apply
方法负责将A
类型的实例l
和r
转换为其泛型HList
表示形式,并调用这些HList
上的SimpleDelta
来计算差值。R
类型是A
的泛型HList
表示形式。隐式参数genA: Generic.Aux[A, R]
表示该方法需要在A
和R
之间进行转换。- 此外,该方法声明一个隐式参数
delta: SimpleDelta[R]
。这是计算l
和r
的两个泛型HList
表示之间的差异所必需的。 - 该方法的返回类型为
delta.Out
。请注意,此处使用依赖类型,因为delta
参数的实际Out
类型成员未知 – 它取决于实际的类型A
及其HList
表示R
。
编译SimpleDelta(homer, ned)
时会发生什么?
- 类型化调用
SimpleDelta.apply[Character, R](homer, ned)
。 - 编译器尝试查找隐式参数的匹配值
genA: Generic.Aux[Character, R]
和delta: SimpleDelta[R]
。 - 无形状为案例类
A
提供了隐式Generic.Aux[A, R]
。这种机制相当复杂,超出了这个问题的范围,但相关的是Character
类型的泛型HList
表示是String :: Int :: Address :: HNil
的,因此编译器可以推断出genA
的类型Generic.Aux[Character, String :: Int :: Address :: HNil]
。这反过来意味着类型变量R
可以推断为String :: Int :: Address :: HNil
。 - 现在类型
R
是已知的,编译器将尝试解析参数delta: SimpleDelta[R]
的隐式值,即SimpleDelta[String :: Int :: Address :: HNil]
.当前作用域不包含任何具有此返回类型的隐式方法或具有此类型的值,但编译器还将查阅参数类型SimpleDelta
的隐式作用域,其中包括配套对象SimpleDelta
(请参阅 Scala 文档中的隐式参数部分)。 - 编译器在
SimpleDelta
对象中搜索提供SimpleDelta[String :: Int :: Address :: HNil]
类型的对象的值或方法。唯一的匹配方法是hconsDelta
,因此编译器将参数值delta
连接到此方法的调用。请注意,HList
分解为头和尾:hconsDelta[String, Int :: Address :: HNil, DT](implicit tailDelta: Aux[Int :: Address :: HNil, DT])
。 - 为了填充隐式
tailDelta
参数,编译器将继续查找匹配的隐式方法。这样,HList
被递归解构,直到只剩下HNil
并调用hnilDelta
。 - 在"向上"时,
DT
类型变量将被推断为Diff[Int] :: Diff[Address] :: HNil
,返回类型hconsDelta
SimpleDelta[String :: Int :: Address :: HNil] {type Out = Diff[String] :: Diff[Int] :: Diff[Address] :: HNil]}
。因此,apply
的返回类型推断为Diff[String] :: Diff[Int] :: Diff[Address] :: HNil
。