Type-level selection sort in shapeless

To celebrate my talk on shapeless being selected for this year’s Northeast Scala Symposium in Boston, I thought I’d share something entertaining (and slightly whimsical) with you — compile time selection sort at the type-level!

Selection sort is just about the simplest possible sorting algorithm — select the least element from your unsorted list as the first element of the sorted list, then recursively sort the remainder — so I’ll take it as a given, and focus on the interesting bit … how can this possibly be done at the type-level?

The first thing we need is a type-level representation of lists of sortable things. For that, we’re going to use an `HList of type-level natural numbers.

import shapeless._
import Nat._
import HList._

def typed[T](t: => T) {}

val unsorted = _3 :: _1 :: _4 :: _0 :: _2 :: HNil
typed[_3 :: _1 :: _4 :: _0 :: _2 :: HNil](unsorted)

Here unsorted is a list of Nat values, and we can see that its structure is exactly mirrored in its type — in shapeless each Nat value (_0, _1, …) has a corresponding Nat type with the same name (_0, _1, …). We use the typed function here to verify the type of unsorted rather than use a type annotation on its val declaration to ensure that the act of verifying the inferred type doesn’t itself contribute to type inference.

Now we need a way of capturing order over Nat at the type level. We do that using a type class that witnesses the relationship at the type level,

trait LTEq[A <: Nat, B <: Nat]

object LTEq {
  import Nat._0

  type <=[A <: Nat, B <: Nat] = LTEq[A, B]

  implicit def ltEq1 = new <=[_0, _0] {}
  implicit def ltEq2[B <: Nat] = new <=[_0, Succ[B]] {}
  implicit def ltEq3[A <: Nat, B <: Nat](implicit lt: A <= B) =
    new <=[Succ[A], Succ[B]] {}
}

If you’re familiar with Peano arithmetic it should be pretty clear how this works — we have two base cases: _0 is less than or equal to _0, and _0 is less than or equal to the successor of any number; and we have one induction case: if A <= B then A+1 <= B+1. With these definitions in place, the magic of Scala’s implicit resolution allows us to ask the compiler about relationships between different Nat types,

scala> import shapeless._ ; import Nat._ ; import LTEq._
import shapeless._
import Nat._
import LTEq._

scala> implicitly[_2 <= _5] // OK
res0: shapeless.LTEq.<=[shapeless.Nat._2,shapeless.Nat._5] =
  shapeless.LTEq$$anon$5@4fc8927c

scala> implicitly[_4 <= _2] // Does not compile
<console>:17: error: could not find implicit value for parameter
  e: shapeless.LTEq.<=[shapeless.Nat._4,shapeless.Nat._2]
    implicitly[_4 <= _2]
              ^

Before we go any further, let’s define an operation which witnesses whether or not an HList of Nat is in sorted order — we’ll want this to verify that our type-level sort is correct,

trait NonDecreasing[L <: HList]

implicit def hnilNonDecreasing =
  new NonDecreasing[HNil] {}

implicit def hlistNonDecreasing1[H] =
  new NonDecreasing[H :: HNil] {}

implicit def hlistNonDecreasing2[H1 <: Nat, H2 <: Nat, T <: HList]
  (implicit ltEq: H1 <= H2, ndt: NonDecreasing[H2 :: T]) =
    new NonDecreasing[H1 :: H2 :: T] {}

def acceptNonDecreasing[L <: HList](l: L)
  (implicit ni: NonDecreasing[L]) = l

// Verify type-level relations
implicitly[NonDecreasing[_1 :: _2 :: _3 :: HNil]] // OK
implicitly[NonDecreasing[_1 :: _3 :: _2 :: HNil]] // Doesn't compile

// Apply at the value-level
acceptNonDecreasing(_1 :: _2 :: _3 :: HNil)       // OK
acceptNonDecreasing(_1 :: _3 :: _2 :: HNil)       // Doesn't compile

This is a fairly straighforward induction — we have two base cases: an empty list is in sorted order, as is a list of exactly one element; and we have one induction case: a list is in sorted order if its tail is, and if its first element is less than or equal to its second element.

Now we can define an operation to select the least element from an HList of Nat, returning both it and the remainder of the list,

trait SelectLeast[L <: HList, M <: Nat, Rem <: HList] {
  def apply(l: L): (M, Rem)
}

trait LowPrioritySelectLeast {
  implicit def hlistSelectLeast1[H <: Nat, T <: HList] =
    new SelectLeast[H :: T, H, T] {
      def apply(l: H :: T): (H, T) = (l.head, l.tail)
    }
}

object SelectLeast extends LowPrioritySelectLeast {
  implicit def hlistSelectLeast3
    [H <: Nat, T <: HList, TM <: Nat, TRem <: HList]
      (implicit tsl: SelectLeast[T, TM, TRem], ev: TM < H) =
        new SelectLeast[H :: T, TM, H :: TRem] {
          def apply(l: H :: T): (TM, H :: TRem) = {
            val (tm, rem) = tsl(l.tail)
            (tm, l.head :: rem)
          }
        }
}

val (l1, r1) = selectLeast(_1 :: _2 :: _3 :: HNil)
typed[_1](l1)
typed[_2 :: _3 :: HNil](r1)

val (l2, r2) = selectLeast(_3 :: _1 :: _4 :: _0 :: _2 :: HNil)
typed[_0](l2)
typed[_3 :: _1 :: _4 :: _2 :: HNil](r2)

Here we’re working at both the type-level and at the value-level — for each example HList we’re selecting the least value-level Nat and it is assigned the corresponding Nat type. Hopefully the recursive pattern is beginning to look fairly familiar, even in this slightly more complicated case — we have a base case: the least element of a singleton list is its only element; and the least element of a non-singleton list is the least element of its tail unless its head is already the least element of the entire list.

And now we can sort!

trait SelectionSort[L <: HList, S <: HList] {
  def apply(l: L): S
}

trait LowPrioritySelectionSort {
  implicit def hlistSelectionSort1[S <: HList] =
    new SelectionSort[S, S] {
      def apply(l: S): S = l
    }
}

object SelectionSort extends LowPrioritySelectionSort {
  implicit def hlistSelectionSort2
    [L <: HList, M <: Nat, Rem <: HList, ST <: HList]
      (implicit
        sl: SelectLeast[L, M, Rem],
        sr: SelectionSort[Rem, ST]
      ) =
        new SelectionSort[L, M :: ST] {
          def apply(l: L) = {
            val (m, rem) = sl(l)
            m :: sr(rem)
          }
        }
}

def selectionSort[L <: HList, S <: HList](l: L)
  (implicit sort: SelectionSort[L, S]) = sort(l)

val unsorted = _3 :: _1 :: _4 :: _0 :: _2 :: HNil
typed[_3 :: _1 :: _4 :: _0 :: _2 :: HNil](unsorted)
acceptNonDecreasing(unsorted)  // Does not compile!

val sorted = selectionSort(unsorted)
typed[_0 :: _1 :: _2 :: _3 :: _4 :: HNil](sorted)
acceptNonDecreasing(sorted)    // Compiles!

As you can see, the static type of the sorted value reflects the fact that it is in sorted order, and that consequently, unlike the initial unsorted value, it has the correct type to be passed to the acceptNonDecreasing function that we wrote earlier.

Update — Joni Freeman has done a nice job of porting the algorithm used to Prolog here.

If you’ve made it this far, and want to play around with it yourself, you can find shapeless on github along with the example source this article is based on. You’ll also find a mailing list for discussion around shapeless here. Oh, and if you’re going to be at Nescala then I’ll see you there …

Discussion

Daniel Sobral (@dcsobral) – Fri, 27th Jan 2012, 4:25pm GMT

I’m speechless. ;-)

There seems to be one bug: ev : TM < H on object SelectLeast — there’s no < type defined anywhere.

Kenji Yoshida (@xuwei_k) – Fri, 27th Jan 2012, 4:57pm GMT

Very interesting post!

But I think maybe this examples also possible using the sing library (sing is a Type-Level Metaprogramming Library).

Similar examples in sing here and here.

Miles Sabin (@milessabin) – Fri, 27th Jan 2012, 6:50pm GMT

@dcsobral Yes, well spotted. Not actually a bug though … you can find the definition of LT[A, B] here. It’s very similar to the definition of LTEq.

Miles Sabin (@milessabin) – Fri, 27th Jan 2012, 6:55pm GMT

@xuwei_k Yes, you’re right, there are a lot of overlaps between shapeless and Sing. I think there are a lot of differences between the implementation techniques used however. Exploring ways that singleton types can be put to use even further is something that’s definitely on my agenda for shapeless.

Alexander Lehmann – Sat, 28th Jan 2012, 2:10pm GMT

That’s like Prolog taken to the next level. Thanks once again for another awesome post.

Contributions should add to the discussion (no "This sucks!" or "Great post!", please tweet @milessabin instead) and you should abide by the Typelevel code of conduct. Minor corrections to the article and discussion can be made via edits and pull requests on github. Github-based discussion by Hubbub.