我很确定我在这里遗漏了一些东西,因为我对Shapeless很新,而且我正在学习,但是Aux技术何时需要呢?我看到它用于通过将type
语句提升到另一个"伴侣" type
定义的签名来公开语句.
trait F[A] { type R; def value: R } object F { type Aux[A,RR] = F[A] { type R = RR } }
但是这不等于将R放入F的类型签名中吗?
trait F[A,R] { def value: R } implicit def fint = new F[Int,Long] { val value = 1L } implicit def ffloat = new F[Float,Double] { val value = 2.0D } def f[T,R](t:T)(implicit f: F[T,R]): R = f.value f(100) // res4: Long = 1L f(100.0f) // res5: Double = 2.0
我发现,如果可以在参数列表中使用它们,那么路径依赖类型会带来好处,但我们知道我们做不到
def g[T](t:T)(implicit f: F[T], r: Blah[f.R]) ...
因此,我们仍然被迫在签名中添加一个额外的类型参数g
.通过使用该Aux
技术,我们还需要花费额外的时间来编写伴侣object
.从使用的角度来看,像我这样的天真用户会觉得使用路径依赖类型没有任何好处.
我只能想到一种情况,即对于给定的类型级计算,返回多个类型级别的结果,并且您可能只想使用其中一种.
我想这一切归结为我在我的简单例子中忽略了一些东西.
这里有两个不同的问题:
为什么在某些类型的类中,Shapeless使用类型成员而不是类型参数?
为什么Shapeless Aux
在这些类型类的伴随对象中包含类型别名?
我将从第二个问题开始,因为答案更直接:Aux
类型别名完全是语法上的便利.你永远不必使用它们.例如,假设我们想要编写一个方法,只有在使用两个具有相同长度的hlists调用时才会编译:
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
别名使我们的代码更易于阅读和编写,但它们并没有以任何有意义的方式改变我们能够或不能完成的代码.
第一个问题的答案有点复杂.在许多情况下,包括my sameLength
,Out
作为类型成员而不是类型参数没有任何优势.因为Scala 不允许多个隐式参数部分,所以N
如果我们要验证两个Length
实例具有相同的Out
类型,我们需要成为我们方法的类型参数.那时,Out
on Length
也可能是一个类型参数(至少从我们作为作者的角度来看sameLength
).
在其他情况下,虽然,我们可以采取的一个事实,即无形有时(我会具体说说优点,其中在某一时刻)使用类型成员,而不是类型参数.例如,假设我们要编写一个方法,该方法将返回一个函数,该函数将指定的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
是一个类型参数,而不是一个类型的成员,我们不得不写这样的事情,而不是:
// 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类都有一个通用表示,因此Generic
有一个类型参数(对于case类型)和一个类型成员(对于表示类型); 每个HList
都有一个长度,所以Length
有一个类型参数和一个类型成员等.
使用唯一确定的类型类型成员而不是类型参数意味着如果我们只想将它们用作路径依赖类型(如converter
上面的第一个),我们可以,但如果我们想要使用它们就像它们是类型参数一样,我们总是可以写出类型细化(或语法更好的Aux
版本).如果Shapeless从一开始就使这些类型的参数类型化,那就不可能朝相反的方向发展.
作为旁注,类型类的类型"参数"之间的这种关系(我使用引号,因为它们可能不是文字Scala意义上的参数)在Haskell这样的语言中称为"函数依赖",但你不应该'感觉你需要了解Haskell中关于功能依赖的任何信息才能得到Shapeless中正在发生的事情.