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

时间:2015-12-31 10:04:17

标签: scala types shapeless type-level-computation

我很确定我在这里遗漏了一些东西,因为我对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。从使用的角度来看,像我这样的天真用户会觉得使用路径依赖类型没有任何好处。

我只能想到一种情况,即对于给定的类型级计算,返回多个类型级别的结果,并且您可能只想使用其中一种。

我想这一切归结为我在我的简单例子中忽略了一些东西。

1 个答案:

答案 0 :(得分:53)

这里有两个不同的问题:

  1. 为什么在某些类型的类中,Shapeless使用类型成员而不是类型参数?
  2. 为什么Shapeless在这些类型类的伴随对象中包含Aux类型别名?
  3. 我将从第二个问题开始,因为答案更直接: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别名使我们的代码更易于阅读和编写,但它们不会改变我们能够或不能改变任何有意义的代码方式。

    第一个问题的答案有点复杂。在很多情况下,包括我的sameLengthOut作为类型成员而不是类型参数没有任何优势。因为Scala doesn't allow multiple implicit parameter sections,如果我们要验证两个N实例具有相同的Length类型,我们需要Out作为我们方法的类型参数。此时,Out上的Length也可能是一个类型参数(至少从我们作为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&#39; s 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从一开始就制作了这些类型的参数,就不可能向相反的方向发展。

    作为旁注,类型类&#34;参数&#34;之间的这种关系。 (我使用引号,因为它们可能不是文字Scala意义上的参数)在Haskell这样的语言中被称为"functional dependency",但你不应该感觉像你需要了解Haskell中关于函数依赖的任何内容,以了解Shapeless中发生的事情。