まずいきなりコップ本第二版から引用(191ページ)
親クラスの具象メンバーをオーバーライドするすべてのメンバーにこの修飾子を付けなくてはならない。また、同じ名前の抽象メンバーを実装する場合、この修飾子はオプションとなるため、付けても付けなくてもよい。
この「付けても付けなくてもよい」についての話。「すでに実装があるメソッドをoverride」する場合は、必ずoverride修飾子は付けないといけない(コンパイルエラー)ので、その場合は関係ない。
あくまで「抽象メソッドをoverride」する場合に、付けるべきか?付けないべきか?の話。Scalaのversionによって違いは無いと思うけど、一応最新安定版の2.10.3という前提で。
「付けても付けなくてもよい」はある意味正しいです。しかし「ではどっちにするべきなのか?」「付けるのと付けないのとで、本当に違いがないのか?」
について言及しているものが、コップ本含めweb上でもほとんど見たことがない
*1
*2
自分は長年以下の様な考えでした
たしかに「付けても付けなくてもよい」けど、
- 付けたほうが、抽象メソッドをそこで実装しているとひと目でわかる
- 万が一「overrideをしようとしたつもりが、間違って微妙にシグネチャが異なる別メソッドを定義」してしまうというミスを防ぐためにも、どちらかというとチェックの意味で付けたほうがいいか。特に付けることによりデメリットなさそうだし
がしかし、レアケースだが「override修飾子を付けないほうが良い場合もあるのでは?」ということに気づいた、ので、それの説明を今書いている。いわゆる菱型継承の場合。
まず先にコードを示す。以下はコンパイル通るが
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 // 後からmixinされたほうが優先なので、Bの実装が使われる
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 ^ */
つまり
「菱型継承する可能性があって、その場合に、traitのmixinの順番によって呼ばれるメソッドが決定されるのではなく、衝突したら明示的に再度overrideすることを強制させたいケース」
である。
「いやそんなケースほとんどないでしょ?」と思うかもしれないが、あるのである。Scalazで・・・。
Scalazにおいて
- Functorはmapという抽象メソッドを1つ持っている
- TraverseはFunctorを継承している
- Traverseにおいて、他のメソッドからmapは実装できるので、以下のようにoverrideされて定義されている
override def map[A,B](fa: F[A])(f: A => B): F[B] = traversal[Id](Id.id).run(fa)(f)
ここから、さらに細かいScalazの事情を知らないと理解できない話なのだが
- typeclassのインスタンスを定義する場合に、他のtypeclassのインスタンスを要求する場合と、そうでない場合がある
- 他のtypeclassのインスタンスを要求しないとは、たとえばList, Optionなど
- 他のtypeclassのインスタンスを要求する場合とは、OneAnd, OneOr, Cokleisli, Kleisli, Coproduct, EitherT, ListT, OptionT, StreamTなど
そして、そういった「他のtypeclassのインスタンスを要求する場合」には、Scalazの慣習としてprivate traitを定義して、実装を共有するようにしている。typeclass毎に要求するtypeclassが異なる*4ので、private traitが大量に定義してある。
具体的には以下のような感じ(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がOneOrFunctorを継承してるのは、「OneOrFunctorでoverrideしたmapの実装を使うため」のはずである。
しかし、実際はwith Traverse
が最後に来ているので、Traverseでの実装が使われてしまうことになる。つまりOneOrFunctorを継承してる意味がない。「Traverseでのmap実装」が使われるのは意図していないことのはずである。というわけでつい先程直した
https://github.com/scalaz/scalaz/commit/db3082f1895
べつにTraverseでのmap実装が使われてしまうからといって、致命的なバグというわけではない。しかし、Traverseでのmap実装よりも、overrideしてもっと効率的なmapの実装を提供できる場合がほとんどである。
これは、同じくmapのデフォルト実装を提供している、ApplicativeやMonadの場合にも当てはまる。
もし、Traverseでのmapの実装にoverride修飾子がついてなかったら、OneOrFunctorとTraverseの両方でmapを実装していて衝突するので、コンパイルエラーになったはずである。
このように「抽象メソッドをoverrideした場合にoverride修飾子を付けるか、付けないか?」によって、菱型継承した場合のコンパイルエラーになるかどうかの挙動が異なる。
現状のScalazで
- 親のtypeclassのメソッドのデフォルト実装を提供できる(Haskellではこれができないので、そういう機能を入れる?という話がでているらしい)
というのは便利な反面
- mixinした場合にどれが使われているかがわかりづらい。traitのmixinの順によって、どの実装が使われるか変わってしまう
- 結局親のtypeclassのデフォルト実装よりも、大抵の場合効率のよい実装が存在するので、それほどデフォルト実装使わない
という微妙なジレンマがある。
まぁこの「typeclassのデフォルト実装」に関しては、色々メリットもデメリットもあり、素晴らしい解決策はないというか、色々考えて今のような実装に落ち着いてるので、そんな微妙なジレンマと闘いつつ、日々このようにScalazの地味な改善をしています・・・。
「抽象メソッドはoverrideするが、衝突した場合にtraitのmixin順で決まるのではなく、コンパイルエラーにする」
ということを示す機能があればいいのかなぁ・・・。というか個人的には「traitのmixinの順で意味が変わる」という仕様が嫌いなので、その仕様自体なくなって欲しい・・・