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

标签 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 .从使用的角度来看,对于像我这样的天真的用户来说,使用依赖于路径的类型根本没有任何好处。

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

我想这一切都归结为我忽略了我的简单示例中的某些内容。

最佳答案

这里有两个单独的问题:

  • 为什么在某些类型类中,Shapeless 在某些情况下使用类型成员而不是类型参数?
  • 为什么Shapeless包括Aux这些类型类的伴随对象中的类型别名?

  • 我将从第二个问题开始,因为答案更直接: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 type 类有一个类型参数(对于 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与以这种方式写出类型改进相比,version 有几个优点:噪音较小,并且不需要我们记住类型成员的名称。不过,这些纯粹是符合人体工程学的问题——Aux别名使我们的代码更容易阅读和编写,但它们不会以任何有意义的方式改变我们可以或不能对代码做什么。

    第一个问题的答案稍微复杂一些。在很多情况下,包括我的 sameLength , 没有优势 Out是类型成员而不是类型参数。因为 Scala doesn't allow multiple implicit parameter sections , 我们需要 N如果我们想验证两个 Length 是否可以作为我们方法的类型参数实例具有相同的 Out类型。此时,OutLength也可能是一个类型参数(至少从我们作为 sameLength 的作者的角度来看)。

    但是,在其他情况下,我们可以利用 Shapeless 有时(我将在稍后具体讨论的地方)使用类型成员而不是类型参数的事实。例如,假设我们要编写一个方法,该方法将返回一个函数,该函数会将指定的案例类类型转换为 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]
    

    这使得它基本上毫无值(value),因为重点是让通用机器弄清楚表示。

    通常,只要类型由类型类的其他参数唯一确定,Shapeless 就会将其设为类型成员而不是类型参数。每个 case 类都有一个通用表示,所以 Generic有一个类型参数(用于 case 类类型)和一个类型成员(用于表示类型);每HList具有单一长度,所以 Length有一个类型参数和一个类型成员等。

    使唯一确定的类型成为类型成员而不是类型参数意味着如果我们只想将它们用作依赖于路径的类型(如上面的第一个 converter),我们可以,但是如果我们想像它们一样使用它们类型参数,我们总是可以写出类型细化(或语法更好的 Aux 版本)。如果 Shapeless 从一开始就将这些类型设为类型参数,则不可能反其道而行之。

    作为旁注,类型类的类型“参数”(我使用引号,因为它们可能不是字面 Scala 意义上的参数)之间的这种关系称为 "functional dependency"在像 Haskell 这样的语言中,但你不应该觉得你需要了解 Haskell 中关于函数依赖的任何东西来了解 Shapeless 中发生的事情。

    关于scala - 为什么类型级计算需要 Aux 技术?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34544660/

    相关文章:

    PHP/甲骨文 : Time representation

    scala - 编译器无法找到单例符号的隐式 Witness.Aux[_]

    scala - 与 Poly 的联产品

    scala - 如何从 Scala 调用重载的 Java 可变参数方法?

    scala - 如何在 Scala 中将 Option[Seq[A]] 转换为 List[A]

    scala - 列表值的函数计算

    generics - F# 中序列表达式中的类型推断

    c++ - "Error: incomplete type "unsigned char[] ""在 Solaris 机器上用 SUN C++ 编译时

    Scala 类型约束来检查参数值

    Scala self 类型特征实例化