这里有两个单独的问题:
-
为什么在某些类型类中,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公司
在隐式参数列表中键入member,但这只是一种方便,以下内容完全相同:
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 }
) = ()
这个
辅助的
version比以这种方式写出类型细化有几个优点:它噪音更小,而且不需要我们记住类型成员的名称。他认为,这些纯粹是人体工程学问题
辅助的
别名使我们的代码更易于阅读和编写,但它们不会以任何有意义的方式改变我们可以或不能使用代码的内容。
第一个问题的答案稍微复杂一些。在许多情况下,包括我的
sameLength
,没有优势
Out
是类型成员而不是类型参数。因为Scala
doesn't allow multiple implicit parameter sections
,我们需要
N
如果我们想验证这两个参数
长
实例具有相同的
出来
类型此时
出来
在…上
长
也可以是一个类型参数(至少从作者的角度来看
相同长度
).
然而,在其他情况下,我们可以利用这样一个事实,即有时无形状(我将具体讨论
哪里
稍后)使用类型成员而不是类型参数。例如,假设我们要编写一个方法,该方法将返回一个函数,该函数将指定的case类类型转换为
H列表
:
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
如果是一个类型参数而不是类型成员,则我们必须编写如下内容:
// Doesn't compile
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类都有一个泛型表示,因此
通用的
具有一个类型参数(用于case类类型)和一个类型成员(用于表示类型);每一个
H列表
只有一个长度,所以
长
具有一个类型参数和一个类型成员等。
使唯一确定的类型成为类型成员而不是类型参数意味着如果我们只想将它们用作路径依赖类型(如第一个
converter
上面),我们可以,但是如果我们想把它们当作类型参数来使用,我们总是可以写出类型精化(或者语法更好
辅助的
版本)。如果Shapeless从一开始就让这些类型类型参数,那么就不可能朝着相反的方向发展。
作为旁注,类型类的类型“参数”(我使用引号,因为它们可能不是
参数
在文字Scala意义上)被称为
"functional dependency"
在Haskell这样的语言中,但你不应该觉得你需要了解Haskell的函数依赖性,才能了解Shapeless中的情况。