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.