为什么在类型级计算中需要Aux技术?

48

我相信我错过了一些东西,因为我对Shapeless还很陌生并且在学习中,但是何时实际上需要使用Aux技术呢?我看到它被用来通过将其提升到另一个“伴随”type定义的签名中来公开一个type语句。

trait F[A] { type R; def value: R }
object F { type Aux[A,RR] = F[A] { type R = RR } }

但这不是在 F 的类型签名中加入 R 吗?
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的签名中添加一个额外的类型参数。通过使用技术,我们还需要花费额外的时间编写伴生对象object。从使用的角度来看,对于像我这样的初学者,使用路径相关类型似乎没有任何好处。
只有一种情况我能想到,那就是在给定类型级计算中返回多个类型级结果,并且您可能只想使用其中一个。
我想这一切都归结于我在简单示例中忽略了某些东西。
1个回答

58
这里有两个不同的问题:
  1. 为什么Shapeless在某些类型类中有时使用类型成员而不是类型参数?
  2. 为什么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别名使我们的代码更易于阅读和编写,但它们并不会以任何有意义的方式改变我们可以或不能够用代码做什么。

对第一个问题的回答稍微复杂一些。在许多情况下,包括我的sameLengthOut作为类型成员与类型参数相比没有优势。因为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。如果GenericRepr是一个类型参数而不是一个类型成员,我们就必须像这样编写:

// 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 class 都有单个通用表示,因此 Generic 有一个类型参数(用于 case class 类型)和一个类型成员(用于表示类型);每个 HList 都有单个长度,因此 Length 有一个类型参数和一个类型成员,等等。
将唯一确定的类型作为类型成员而不是类型参数意味着,如果我们只想将它们用作路径相关类型(如上面的第一个 converter),我们可以这样做,但如果我们想将它们用作类型参数,我们总是可以编写类型细化(或语法更好的 Aux 版本)。如果 Shapeless 从一开始就将这些类型作为类型参数,就不可能走相反的方向。
作为一则附注,类型类的类型“参数”(我使用引号,因为它们可能不是字面上的Scala意义上的“参数”)之间的这种关系在像Haskell这样的语言中被称为"函数依赖",但你不必感觉需要理解Haskell中的函数依赖才能理解Shapeless中发生了什么。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接