了解 Scala 类型系统中的 Aux 模式

Understanding the Aux pattern in Scala Type System

这个问题以前可能会被问到和回答过,但我想通过一个例子来理解这个问题,但我无法推断出 Aux 模式在哪些方面可能会有帮助!所以这是特征:

trait Foo[A] {
  type B
  def value: B
}

为什么我有一个类型绑定到值函数的 return 类型?我这样做有什么好处?特别是,我应该在哪里使用这种模式?

想象一个用于获取任何元组的最后一个元素的类型类。

trait Last[A] {
  type B
  def last(a: A): B
}

object Last {
  type Aux[A,B0] = Last[A] { type B = B0 }

  implicit def tuple1Last[A]: Aux[Tuple1[A],A] = new Last[Tuple1[A]] {
    type B = A
    def last(a: Tuple1[A]) = a._1
  }

  implicit def tuple2Last[A,C]: Aux[(A,C),C] = new Last[(A,C)] {
    type B = C
    def last(a: (A,C)) = a._2
  }

  ...
}

类型B总是依赖于类型A,这就是为什么A是类型类的输入类型而B是输出类型的原因。

现在,如果您想要一个可以根据最后一个元素对任何元组列表进行排序的函数,您需要访问同一参数列表中的 B 类型。这是主要原因,在 Scala 的当前状态下,为什么需要 Aux 模式:目前不可能在与 last 相同的参数列表中引用 last.B 类型定义,也不可能有多个隐式参数列表。

def sort[A,B](as: List[A])(implicit last: Last.Aux[A,B], ord: Ordering[B]) = as.sortBy(last.last)

当然你总是可以完整地写出 Last[A] { type B = B0 },但显然这很快就会变得非常不切实际(想象一下添加更多具有依赖类型的隐式参数,这在 Shapeless 中很常见);这就是 Aux 类型别名的来源。

启动 Scala 3 dependent types are supported within the same parameter list, which seems to make Aux pattern unnecessary, for example,Jasper-M 的代码片段简化为

trait Last[A]:
  type B
  def last(a: A): B

given [A]: Last[Tuple1[A]] with
  type B = A
  def last(a: Tuple1[A]) = a._1

given [A, C]: Last[(A,C)] with
  type B = C
  def last(a: (A,C)) = a._2

def sort[A](as: List[A])(using last: Last[A], ord: Ordering[last.B]) = as.sortBy(last.last)

sort(List(("ffle",3), ("fu",2), ("ker",1)))
// List((ker,1), (fu,2), (ffle,3))

注意 last.B 的用法,其中 last 是来自

中相同参数列表的值
def sort[A](as: List[A])(using last: Last[A], ord: Ordering[last.B])