Fork me on GitHub

Graceless Failures

Tips, tricks, missteps, and minor revelations on the path to Scala wisdom.

Propagating View Bounds to Traits

View bounds provide the means by which to achieve ad-hoc polymorphism in Scala. One way to think of view bounds (these are also equivalent to type classes in Haskell) is that they provide a sort-of run-time polymorphism. To review:

class A[T <% String] {
  // Methods here can now operate operate treating any type `T' as
  // `String'

  def sayHey(x: T): String = "hey! " + (x:String)
}

Here, you can only instantiate the class A with the type T if there is, at the time of instantiation an implicit function available of type T => String.

Note that the <%-notation is syntactical sugar in Scala, making the previous example equivalent to:

class A[T](implicit tToString: (T => String)) {
  // Methods here can now operate operate treating any type `T' as
  // `String'

  def sayHey(x: T): String = "hey! " + (x:String)
}

However, view bounds break down when you introduce self-type annotated traits.

class A[T <% String] {
  // Methods here can now operate operate treating any type `T' as
  // `String'
}

trait SomeTrait[T] { self:A[T] =>
  // this fails to compile:
  def sayHey(x: T): String = "hey! " + (x:String)
}

Even though we used a self-type annotation, the compiler doesn’t make available the implicit function available in the “parent” class. The problem is the way the syntax-sugar works here. It binds the implicit to the lexical scope of the parent class.

However, if we could change the implicit to be a val instead, we’d be set. This works:

class A[T](implicit val tToString: (T => String)) {
  // Methods here can now operate operate treating any type `T' as
  // `String'

  def sayHey(x: T): String = "hey! " + (x:String)
}

trait SomeTrait[T] { self:A[T] =>
  // this fails to compile:
  def sayHey(x: T): String = "hey! " + (x:String)
}

However, you forgo the succinctness of the view-bound notation, and it doesn’t nest: adding another trait

trait SomeSubTrait[T] { self:SomeTrait[T] =>
  // this fails to compile:
  def sayHeyAgain(x: T): String = "hey again! " + (x:String)
}

will again fail to compile.

I ended up working around this by a simple trick (and, I think, a prettier solution to boot) by simply “exporting” the implicit definition.

class A[T <% String] {
  // Methods here can now operate operate treating any type `T' as
  // `String'

  implicit def tToString(t: T): String = t
  def sayHey(x: T): String = "hey! " + (x:String)
}

trait SomeTrait[T] { self:A[T] =>
  // the implicit from `A' is now available here.
  def sayHey(x: T): String = "hey! " + (x:String)
}

This still does not nest by itself, but you can work around that by again re-exporting the implicit made available to the trait.