ScalaでもJavaでも、overrideする際に、sub typeでoverrideすることが可能です。 (すごく古い1.4以前のJavaでは不可能だったはずだが)
Javaの仕様書で英語だと covariant return type
というはず?の機能です。
https://docs.oracle.com/javase/specs/jls/se11/html/jls-8.html#d5e14373
$ jshell | Welcome to JShell -- Version 17.0.6 | For an introduction type: /help intro jshell> interface A { Object a(); } | created interface A jshell> class B implements A { @Override public String a() { return "aaa";} } | created class B jshell> String x = new B().a() x ==> "aaa"
Welcome to Scala 3.3.1 (17.0.6, Java OpenJDK 64-Bit Server VM). Type in expressions for evaluation. Or try :help. scala> trait A { def a: Object } clas// defined trait A scala> class B extends A { override def a: String = "aaa" } // defined class B scala> (new B).a val res0: String = aaa
さて、ScalaにはJavaと違って、ローカル変数以外でのもっと強い?広い?型推論機能があるため、慣習的にはそれほど推奨されませんが、publicなmethodの戻り値型でも、推論に任せて明示的な型の記述を省略出来てしまいます。
Welcome to Scala 3.3.1 (11.0.20, Java OpenJDK 64-Bit Server VM). Type in expressions for evaluation. Or try :help. scala> class A { | def x = 2 | def y = "abc" | } // defined class A scala> (new A).x val res0: Int = 2 scala> (new A).y val res1: String = abc
ここまでが大前提として、ここからタイトルの「overrideした際の共変戻り値」と「型推論」の関係の説明をするわけですが、
overrideする際に共変の戻り値(親で返していたものよりも、より具体的なsub type)を返しつつも、型を明示せずに型推論に任せた場合は、何に推論されるべきでしょうか?
何と、これがScalaのversionやcompile optionによって異なります。
Welcome to Scala 3.3.1 (11.0.20, Java OpenJDK 64-Bit Server VM). Type in expressions for evaluation. Or try :help. scala> trait A { def a: Object } // defined trait A scala> class B extends A { override def a = "aaa" } // defined class B scala> (new B).a val res0: Object = aaa
Welcome to Scala 2.13.12 (OpenJDK 64-Bit Server VM, Java 11.0.20). Type in expressions for evaluation. Or try :help. scala> trait A { def a: Object } trait A scala> class B extends A { override def a = "aaa" } class B scala> (new B).a val res0: String = aaa
compile optionなしの場合は
となります。このScala 3での変更は、意図した仕様変更です。
さて、Scala 2.12や2.13には -Xsource:3
という、出来るだけ色々をScala 3に似せて、警告を出したり、Scala 3の文法が一部だけ使えたり、そもそもcompile時の挙動というかcompile後の出力自体を変えるオプションがあります。
それを指定すると、かなり色々と変わって、そもそも自分も全部完璧に把握できてないので、全てを説明しませんが、
少なくともこれを指定すると、今回の主題である「overrideした際の共変戻り値」について「型推論」の挙動が変わります。
Welcome to Scala 2.13.11 (OpenJDK 64-Bit Server VM, Java 11.0.20). Type in expressions for evaluation. Or try :help. scala> trait A { def a: Object } trait A scala> class B extends A { override def a = "aaa" } class B scala> (new B).a val res0: Object = aaa
Scala 2.13の途中のどこか(詳細を後で調べたら追記するかも)、から、Scala 3と同じように、型の記述を省略すると、親の型になります。 しかし、これはこれで、出力されるJVMでのbytecodeというかclassfileが変わって、ライブラリ作者からすると、意図せずバイナリ互換が維持しにくくなる元となって、 単に警告が増えるような変更とは違い、おそらく、色々と驚きというか、反響があったと思われます。 (というか自分がtwitterで騒いでたりしましたが)
よって、つい最近リリースされたScala 2.13.12から、なんと、これは -Xsource:3
だけを指定した場合にデフォルトではcompile errorになるようになりました!!!
Welcome to Scala 2.13.12 (OpenJDK 64-Bit Server VM, Java 11.0.20). Type in expressions for evaluation. Or try :help. scala> trait A { def a: Object } trait A scala> class B extends A { override def a = "aaa" } ^ error: under -Xsource:3, inferred Object instead of String [quickfixable] Scala 3 migration messages are errors under -Xsource:3. Use -Wconf / @nowarn to filter them or add -Xmigration to demote them to warnings. Applicable -Wconf / @nowarn filters for this fatal warning: msg=<part of the message>, cat=scala3-migration, site=B.a
エラーメッセージにあるように、他のオプションを同時に指定するなどで回避は可能です。 これ以外にもいくつか同様に、急に(?)compile errorに変わったものがあります。(case classのコンストラクタに対するprivate付与など)
ただ、個人的には、理由を知らないとびっくりするだろうけど、安全側に倒すというか、ユーザーに明示的に何をしてるか選ばせる(型を書かせるか、明示的に警告などにレベルを変えさせる)のは、とても良い変更だと思いました。
親の型のままの方がいいか?sub typeでさらに具体的な型になって欲しいか?は、完全に場合によると思うので、考えて付与しましょう。 sub typeは実装詳細で隠蔽したい場合は親の型のままの方がいいだろうし、あえて具体的な型を露出させていきたい(露出させないとそもそもcompileできない)場合は、そちらを書くようにしましょう。
https://github.com/scala/scala/pull/10439
よって、各種OSSや仕事でも、基本的に抑制はせずに全部、型を書いていく方針で直してます。 ただ、無名クラスでそのシグネチャが表に出ない場合は、だるい気はしますが、まぁ現実的に書ける量だったので、全部書きました。
ちなみに、こういうのを半自動で修正するquickfixという機能がScala 2本体に入ったらしいですが、バグっていてあまり使えないので、使いませんでした。
以下、OSSで修正した例などを貼っておきます
- https://github.com/scalapb/ScalaPB/issues/1576
- https://github.com/typelevel/cats/pull/4508
- https://github.com/scalaz/scalaz/pull/2578
- https://github.com/scalaz/scalaz/pull/2580
- https://github.com/typelevel/scalacheck/pull/1005
- https://github.com/slick/slick/pull/2825
- https://github.com/typelevel/cats-parse/pull/537
- https://github.com/typelevel/cats-effect/pull/3816
- https://github.com/scalatra/scalatra/pull/1569
- https://github.com/gitbucket/gitbucket/pull/3365
- https://github.com/scala/scala-collection-laws/pull/106