Ordering case classes
Scala provides lexicographic Ordering
instances for
TupleN
types when all of the element types have Ordering
instances.
It doesn't provide the same instances for case classes, however—probably
just because lexicographic order isn't what you want for case classes as
often as it is for tuples.
Sometimes you actually do want a lexicographic ordering
for your case classes, though, and Scala unfortunately doesn't provide any nice
boilerplate-free way to create such orderings. This post will provide a
quick sketch of two approaches to filling this gap: one using macros, and
one using Shapeless 2.0's new Generic
machinery
and the TypeClass
type class.
First for a case class to use as a running example, along with some instances:
case class Foo(x: Int, y: String)
val a = Foo(9, "x")
val b = Foo(1, "z")
val c = Foo(9, "w")
val foos = List(a, b, c)
Let's quickly confirm that there's no Ordering[Foo]
already sitting around:
scala> foos.sorted
<console>:14: error: No implicit Ordering defined for Foo.
foos.sorted
^
Yep, we're going to have to take care of this ourselves.
First for the macro solution (note that I'm using quasiquotes, which are available in Scala 2.10 as a plugin):
import scala.language.experimental.macros
import scala.reflect.macros.Context
object OrderingHelper {
implicit def apply[A]: Ordering[A] = macro apply_impl[A]
def apply_impl[A: c.WeakTypeTag](c: Context) = {
import c.universe._
val fields = weakTypeOf[A].declarations.collect {
case sym: MethodSymbol if sym.isCaseAccessor => q"a.${sym.name}"
}
if (fields.isEmpty) {
c.abort(c.enclosingPosition, "Not a case class!")
} else {
val scalaSym = typeOf[Any].typeSymbol.owner
weakTypeOf[A].baseClasses.collectFirst {
case sym
if sym.name.decoded.startsWith("Tuple") && sym.owner == scalaSym =>
c.abort(c.enclosingPosition, "Not needed for tuples!")
} getOrElse c.Expr[Ordering[A]](q"Ordering.by(a => (..$fields))")
}
}
}
Apart from the fact that we need to confirm that we don't have a tuple (which would throw us into an infinitely recursive implicit search and crash the compiler), this is pretty straightforward stuff—we just collect all the case accessors and plop them into a tuple, which we know how to order lexicographically.
One import, and we're done:
scala> import OrderingHelper._
import OrderingHelper._
scala> foos.sorted
res0: List[Foo] = List(Foo(1,z), Foo(9,w), Foo(9,x))
This is all fairly nice and type-safe, in the sense that if our case class has an unordered member, we get a reasonably helpful compile-time error:
scala> trait Unsortable
defined trait Unsortable
scala> case class Bar(u: Unsortable)
defined class Bar
scala> OrderingHelper[Bar]
<console>:20: error: No implicit Ordering defined for (Unsortable,).
OrderingHelper[Bar]
^
And even that could be pretty easily improved. It's still arguably kind
of clunky and ad-hoc, though, which is where Shapeless comes in. With
Lars Hupel's ProductTypeClass
type class, we
can just describe how to build up lexicographic ordering instances:
import shapeless._
implicit object `Ordering's a type class` extends ProductTypeClass[Ordering] {
def product[H, T <: HList](ho: Ordering[H], to: Ordering[T]) =
Ordering.Tuple2(ho, to).on(l => (l.head, l.tail))
def emptyProduct = Ordering.by(_ => ())
def project[F, G](instance: => Ordering[G], to: F => G, from: G => F) =
instance on to
}
And Generic
will take care of (almost all of) the rest.
scala> object OrderingHelper extends TypeClassCompanion[Ordering]
defined module OrderingHelper
scala> import OrderingHelper.auto._
import OrderingHelper.auto._
scala> foos.sorted
res0: List[Foo] = List(Foo(1,z), Foo(9,w), Foo(9,x))
Much nicer!