scala-libraryはなぜ後方バイナリ互換のみならず前方バイナリ互換も維持しないといけないか

そういえば、はっきり書かれたドキュメントが思い当たらない(or 存在していたとしても、改めて日本語で書くことに意味はあるだろうと思う)、ので、書いてみることにする。

自分が知ってる理由の1つとしては、以下のようなことをすると、普通にNoClassDefFoundErrorやNoSuchMethodErrorになるからである。

とりあえず、2.12の場合でmimaでforwards.excludesされている( = methodやclassなどが例外的に追加されている)、パターンで適当に試してみる。今回は2.12の途中のどこかで追加された showAsInfix というアノテーションのclassを参照してみる。

https://github.com/scala/scala/blob/2.12.x/src/library/mima-filters/2.12.0.forwards.excludes

build.sbt

lazy val a = project.settings(
  scalaVersion := "2.12.8"
)

lazy val b = project.settings(
  scalaVersion := "2.12.0"
).dependsOn(a)

a/A.scala

package example

class A {
  def a: AnyRef = scala.annotation.showAsInfix
}

b/B.scala

package example

object B {
  def main(args: Array[String]): Unit = {
    (new A).a
  }
}

project/build.properties

sbt.version=1.2.8

sbt b/run の実行結果

[error] (run-main-0) java.lang.NoClassDefFoundError: scala/annotation/showAsInfix$
[error] java.lang.NoClassDefFoundError: scala/annotation/showAsInfix$
[error]     at example.A.a(A.scala:4)
[error]     at example.B$.main(B.scala:5)
[error]     at example.B.main(B.scala)
[error]     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[error]     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[error]     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[error]     at java.lang.reflect.Method.invoke(Method.java:498)
[error] Caused by: java.lang.ClassNotFoundException: scala.annotation.showAsInfix$
[error]     at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
[error]     at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
[error]     at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
[error]     at example.A.a(A.scala:4)
[error]     at example.B$.main(B.scala:5)
[error]     at example.B.main(B.scala)
[error]     at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
[error]     at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
[error]     at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
[error]     at java.lang.reflect.Method.invoke(Method.java:498)
[error] Nonzero exit code: 1
[error] (b / Compile / run) Nonzero exit code: 1

以下追加説明や、想定されるツッコミへの回答など

  • 同じproject内でscalaVersion揃えないやつなんていないだろ! => 再現が面倒なので同じプロジェクトでやったが、とあるライブラリが、2.12.xで追加された機能使っていて、そのライブラリを使いつつ、自分のプロジェクトでは2.12.yを設定 ( y は x より小さい)、みたいなことをすると発生する
  • 常に最新のversion使うようにすればいいだけでは? => ある意味そのとおりかもしれない?
  • というかsbtが他の普通のライブラリと比べて、Scalaのversionに関して特別扱い(依存してるやつのなかで最新を使うわけではない) しているのが悪いだけでは? => ある意味それもそのとおりかもしれない?

とまぁ、sbtの挙動の話に行き着くわけですが、とはいえsbt関連以外でも前方互換を守っておくと得することは多々あると思うが、とはいえすぐに思いつかないので、他に情報集まったら追記するなどするかもしれません。

ちなみに、なんで今さらこれを書こうか?と思ったのかというと、ScalaMatsuriのハッカソンをキッカケとして、Scala本体にpull reqする(日本人の) 人を数名見かけるのですが、このパターンにより前方バイナリ互換維持しない変更pull req出してるのを見かけるからです。

まぁ日本人に限らず、よくわからずそういうpull req出す人は昔から結構見かけます。 (ある意味、Scala本体 CONTRIBUTING.md の説明が悪い?足りないのか・・・?)

そもそも2.14.xの前方互換後方互換も壊していい開発用branchが作成されたら、そっちにpull req出せばいいだけなのですが、例年 .0 のversion ( = 2.11.0や2.12.0や2.13.0のこと)が出たあとしばらくは、それのバグ修正が中心でまだ開発用branchは作成されないので、今はそういう意味では新機能追加や、(バグ修正だとしても)互換を壊すpull reqをするには時期が悪いです。待ちましょう・・・(という雑な結論でいいのか・・・?)