Hatena::ブログ(Diary)

scalaとか・・・ このページをアンテナに追加 RSSフィード Twitter

2013-12-20

Scalaで抽象メソッドをoverrideする際にoverride修飾子を付けるべきかどうかの是非

| 12:21 | Scalaで抽象メソッドをoverrideする際にoverride修飾子を付けるべきかどうかの是非を含むブックマーク Scalaで抽象メソッドをoverrideする際にoverride修飾子を付けるべきかどうかの是非のブックマークコメント

まずいきなりコップ本第二版から引用(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)

*3



ここから、さらに細かいScalazの事情を知らないと理解できない話なのだが



そして、そういった「他の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の順で意味が変わる」という仕様が嫌いなので、その仕様自体なくなって欲しい・・・

*1:これから書くようなことが、丁寧に解説してある資料があったら、本でもWebでもいいので教えて下さい・・・

*2:コップ本にも、これから説明することがわかりやすく書いてある箇所見当たらなかったのだが・・・

*3:Applicativeなどでも同じようにmapがoverrideされてるので、これから説明することと同じ問題が発生する

*4:例えば、OneOrのFunctorを定義するためには、OneOrの型パラメータのFがFunctorである必要があり、またOneOrのTraverseを定義するためにはFがTraverseである必要がある、など

トラックバック - http://d.hatena.ne.jp/xuwei/20131220/1387509706