代码之家  ›  专栏  ›  技术社区  ›  Edoardo Vacchi

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

  •  46
  • Edoardo Vacchi  · 技术社区  · 9 年前

    我很确定我在这里错过了一些东西,因为我对Shapeless很陌生,我正在学习,但Aux技术实际上是什么时候 必修的 ? 我看到它是用来暴露 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 回复  |  直到 9 年前
        1
  •  57
  •   Travis Brown    9 年前

    这里有两个单独的问题:

    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公司 在隐式参数列表中键入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中的情况。