这里有两个不同的问题:
- 为什么Shapeless在某些类型类中有时使用类型成员而不是类型参数?
- 为什么Shapeless在这些类型类的伴生对象中包含了类型别名?
我将从第二个问题开始,因为答案更直接:Aux类型别名完全是一种语法上的便利。您不必使用它们。例如,假设我们想编写一个只在调用具有相同长度的两个HList时才编译的方法:
import shapeless._, ops.hlist.Length
def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
al: Length.Aux[A, N],
bl: Length.Aux[B, N]
) = ()
Length
类型类有一个类型参数(用于 HList
类型)和一个类型成员(用于 Nat
)。Length.Aux
语法使得在隐式参数列表中引用 Nat
类型成员相对容易,但这只是一种方便——以下代码完全等效:
def sameLength[A <: HList, B <: HList, N <: Nat](a: A, b: B)(implicit
al: Length[A] { type Out = N },
bl: Length[B] { type Out = N }
) = ()
Aux
版本比以这种方式编写类型细化具有几个优点:它更简洁,不需要我们记住类型成员的名称。尽管这些仅仅是人体工程学问题,Aux
别名使我们的代码更易于阅读和编写,但它们并不会以任何有意义的方式改变我们可以或不能够用代码做什么。
对第一个问题的回答稍微复杂一些。在许多情况下,包括我的sameLength
,Out
作为类型成员与类型参数相比没有优势。因为Scala 不允许多个隐式参数部分,如果我们想要验证两个Length
实例具有相同的Out
类型,则需要将N
作为方法的类型参数。此时,Length
上的Out
可能也是一个类型参数(至少从我们作为sameLength
作者的角度来看)。
在其他情况下,我们可以利用Shapeless有时使用类型成员而不是类型参数的事实(我会在稍后具体讨论)。例如,假设我们想编写一个方法,该方法将返回一个函数,该函数将把指定的case类类型转换为
HList
。
def converter[A](implicit gen: Generic[A]): A => gen.Repr = a => gen.to(a)
现在我们可以像这样使用它:
case class Foo(i: Int, s: String)
val fooToHList = converter[Foo]
我们将得到一个漂亮的 Foo => Int :: String :: HNil
。如果Generic
的Repr
是一个类型参数而不是一个类型成员,我们就必须像这样编写:
def converter[A, R](implicit gen: Generic[A, R]): A => R = a => gen.to(a)
Scala不支持类型参数的部分应用,因此每次调用这个(假设的)方法时,我们都需要指定两个类型参数,因为我们想要指定A
:
val fooToHList = converter[Foo, Int :: String :: HNil]
这使得它基本上毫无价值,因为整个重点是让通用机制找出表示方法。
一般来说,每当一个类型由类型类的其他参数唯一确定时,Shapeless 将其作为类型成员而不是类型参数。每个 case class 都有单个通用表示,因此
Generic
有一个类型参数(用于 case class 类型)和一个类型成员(用于表示类型);每个
HList
都有单个长度,因此
Length
有一个类型参数和一个类型成员,等等。
将唯一确定的类型作为类型成员而不是类型参数意味着,如果我们只想将它们用作路径相关类型(如上面的第一个
converter
),我们可以这样做,但如果我们想将它们用作类型参数,我们总是可以编写类型细化(或语法更好的
Aux
版本)。如果 Shapeless 从一开始就将这些类型作为类型参数,就不可能走相反的方向。
作为一则附注,类型类的类型“参数”(我使用引号,因为它们可能不是字面上的Scala意义上的“参数”)之间的这种关系在像Haskell这样的语言中被称为
"函数依赖",但你不必感觉需要理解Haskell中的函数依赖才能理解Shapeless中发生了什么。