curious case of putting override modifier when overriding an abstract method in Scala
This is a translation of Scalaで抽象メソッドをoverrideする際にoverride修飾子を付けるべきかどうかの是非 by Kenji Yoshida (@xuwei_k), a Scalaz committer.
First, a quote from Programming in Scala, 2nd ed. p. 192:
Scala requires [
override] modifier for all members that override a concrete member in a parent class. The modifier is optional if a member implements an abstract member with the same name.
In this post, we’ll discuss this “The modififier is optional.” Since overriding an existing method with implementation requires override modifier, and failure to do so would result to a compiler error, there’s not much to talk about for that case. We’ll focus on whether one should put override modifier or not in the case of overring an abtract method. I don’t think there’s going to be any difference in Scala version, but let’s assume the latest stable 2.10.3.
“The modifier is option” is correct in face value, but I found seen much discussion in the book or on the web1 on which one should be used, or if there is any difference in putting override or not.
For a long while, I used to think: It’s true that “the modifier is optional,” but
- Putting
overridewould make it clear that it’s implementing an abtract method. - Putting
overridecould prevent, thinking that I’m overriding a method, but declaring another method with slightly different signature by mistake. So defensively speaking, it’s preferable to putoverride, especially because there seems to be no downside to it.
However, I realized a rare, but an actual case that not putting override might be preferable, so I’m writing this now. This occurs in so-called diamond inheritance.
Let’s see the code first. This will compile:
trait A{
def foo: Int
}
trait B extends A{
override def foo = 1
}
trait C extends A{
override def foo = 2
}
trait D extends C with B
// Later mixin has precedence, so B's implementation is used
This, on the other hand would result to a compiler error:
trait A{
def foo: Int
}
trait B extends A{
def foo = 1
}
trait C extends A{
def foo = 2
}
trait D extends C with B
/**
error: trait D inherits conflicting members:
method foo in trait C of type => Int and
method foo in trait B of type => Int
(Note: this can be resolved by declaring an override in trait D.)
trait D extends C with B
^
*/
In short, this is a case in which there’s a possibility of a diamond inheritance, and when one would prefer to explicitly override conflicts instead of depending on the mixin order of the traits.
You might ask “how often would would such case arise?” but I see them. In Scalaz.
In Scalaz,
Functorhas one abstract method namedmap.TraverseinheritsFunctor.2- In
Traverse,mapcould be implemented from other methods, so it’s overridden as follows:
override def map[A,B](fa: F[A])(f: A => B): F[B] = traversal[Id](Id.id).run(fa)(f)
Further discussion require a bit of an internal knowledge of Scalaz:
- When defining a typeclass instance, there are cases that require other typeclass instances and the cases that do not.
- Examples of the cases that do not require other typeclass instances:
- Example of the cases that do require other typeclass instances would be OneAnd, OneOr, Cokleisli, Kleisli, Coproduct, EitherT, ListT, OptionT, StreamT etc.
In case other typeclass instances are required, by conventions in Scalaz, we define private traits to share the implementation. Since each typeclass require different typeclasses3, we end up with defining enormous amount of private traits.
For instance, here’s from OneOr as of 7.1.0-M4. https://github.com/scalaz/scalaz/blob/v7.1.0-M4/core/src/main/scala/scalaz/OneOr.scala#L112:
private sealed trait OneOrFunctor[F[_]]
extends Functor[({type λ[α] = OneOr[F, α]})#λ] {
implicit def F: Functor[F]
override def map[A, B](fa: OneOr[F, A])(f: A => B): OneOr[F, B] =
fa map f
}
private sealed trait OneOrTraverse[F[_]]
extends OneOrFunctor[F] with OneOrFoldable[F] with Traverse[({type λ[α] = OneOr[F, α]})#λ] {
implicit def F: Traverse[F]
override def traverseImpl[G[_]: Applicative,A,B](fa: OneOr[F, A])(f: A => G[B]) =
fa traverse f
override def foldMap[A, B](fa: OneOr[F, A])(f: A => B)(implicit M: Monoid[B]) =
fa.foldMap(f)
}
OneOrTraverse is inheriting OneOrFunctor to use the implementation of map overridden by OneOrFunctor. Or at least, that’s the intention.
But in actuality, because with Traverse is at the end, so the implementation from Traverse is being used. In other words, inheriting OneOrFunctor is rendered pointless. It’s only by accident that map implementation from Traverse is included. So I just fixed it: https://github.com/scalaz/scalaz/commit/db3082f1895
It’s not exactly a critical bug if the map implementation from Traverse is being used. But in most cases we can provide more efficient implementation of map than that of Traverse.
This logic applies for Applicative and Monad, which also provide default implementation of map.
Had it been the case that the map implementation of Traverse did not have override modifier, the map implementation of OneOrFunctor and Traverse would conflict, and it would result in a compiler error.
Thus, in the case of diamond inheritance, whether or not putting override modifier would make the difference of it being a compiler error or not.
In current Scalaz:
- We can provide default implementation for parent typeclasses (Haskell can’t do this, and apparently there’s been some discussion on putting similar function in.)
This is convenient, but on the other hand:
- It’s hard to tell which implementation ends up being used. It’s dependent on mixin order of the traits.
- In most cases, there are more efficient implementation, so the default implementation isn’t as often.
This is a subtle conundrum. The topic of default implementation for typeclass has both pros and cons to it, that there’s no clear panacea. Scalaz ended up in the current implementation after taking various issues into consideration, so I continue to forge on making very minor improvements like this, while fighting suble conundrums like this.
A feature of marking the implementations of abtract methods override, but make it a compiler error when they conflict instead of using the mixin order of the traits may be useful. But personally speaking, I hate the very spec of semantics depending on the mixin orders, so I wish that went away from the spec altogether.
-
Please let me know if there’s any material covering this topic whether it’s a book or online. ↩︎
-
Applicativealso overridesmap, so similar problem arises. ↩︎ -
For instance, to define
FunctorforOneOrthe type parameterFneeds to be aFunctor, but to defineTraverseforOneOrFneeds to be aTraverse. ↩︎