Understanding the Aux pattern in Scala Type System
Imagine a typeclass for getting the last element of any tuple.
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
}
...
}
The type B
always depends on the type A
, that's why A
is an input type of the typeclass and B
is an output type.
Now if you want a function that can sort any list of tuples based on the last element you need access to the B
type in the same argument list. That's the main reason, in the current state of Scala, why you need the Aux
pattern: currently it's not possible to refer to the last.B
type in the same parameter list as where last
is defined, nor is it possible to have multiple implicit parameter lists.
def sort[A,B](as: List[A])(implicit last: Last.Aux[A,B], ord: Ordering[B]) = as.sortBy(last.last)
Of course you can always write Last[A] { type B = B0 }
out in full, but obviously that becomes very impractical very quickly (imagine adding a couple more implicit parameters with dependent types, something that's very common with Shapeless); that's where the Aux
type alias comes in.
Starting Scala 3 dependent types are supported within the same parameter list, which seems to make Aux
pattern unnecessary, for example, Jasper-M's snippet simplifies to
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))
Note the usage of last.B
where last
is a value coming from the same parameter list in
def sort[A](as: List[A])(using last: Last[A], ord: Ordering[last.B])