Scalaでtraitにメソッド追加するとバイナリ互換壊れるけどabstract classだと壊れないという例

プログラマなら論よりコードで示すべきですよね
(英語力ないだけです、すいません)


だいだい最小限の構成にしたつもりなので、以下のリポジトリ見てみてください

https://github.com/xuwei-k/binary-incompatibility-sample

とあるライブラリの最初のversion( 0.1 )

trait Foldable[F[_]]{
  def foldLeft[A, B](fa: F[A], z: B)(f: (B, A) => B): B
}

次のversionでtoListメソッド足す(こっちを0.2とする)

trait Foldable[F[_]]{
  def foldLeft[A, B](fa: F[A], z: B)(f: (B, A) => B): B
  def toList[A](fa: F[A]): List[A] =
    foldLeft(fa, List.empty[A])((xs, x) => x :: xs).reverse
}


そのライブラリに依存した別のライブラリで、version0.1に依存してこんな定義したとする

val vector = new Foldable[Vector] {
  def foldLeft[A, B](fa: Vector[A], z: B)(f: (B, A) => B): B =
    fa.foldLeft(z)(f)
}


その「VectorのFoldableのインスタンスを定義したライブラリ」はFoldableのtraitの0.1の定義でコンパイルしたまま再コンパイルせず

を、以下のように混ぜて使おうとする

vector.toList(Vector(1, 2, 3))

以下のように死ぬ

[error] (run-main-0) java.lang.AbstractMethodError: com.example.FoldableInstances$$anon$1.toList(Ljava/lang/Object;)Lscala/collection/immutable/List;
java.lang.AbstractMethodError: com.example.FoldableInstances$$anon$1.toList(Ljava/lang/Object;)Lscala/collection/immutable/List;
    at com.example.Main$.main(Main.scala:6)
    at com.example.Main.main(Main.scala)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)


「え、それabstract classでも同じじゃないの?」と思うかもしれませんが、そうではありません。Foldableの定義が0.1と0.2で両方abstract classだったら動きます。上記githubの別branch参照 https://github.com/xuwei-k/binary-incompatibility-sample/tree/abstract-class

なぜそうなるか興味を持った人は、traitがどのようにJVMのclassファイルに翻訳されるか調べてみよう(๑•̀ㅂ•́)و✧


なぜわざわざ作ったかというと、scalazで7.1ブランチに、こういう互換ないpull reqきたからです

https://github.com/scalaz/scalaz/pull/1043

「abstract classだと崩れないなら、最初からscalazでもFoldableをそうしておけばいいじゃない?」

と思うかもしれませんが、現状の実装の都合上、たとえば

  • TraverseはFoldableとFunctorを継承
  • MonadはBindとApplicativeを継承

など、scalazでの型クラスは「多重継承」を使って実現されている部分があるので、すべての型クラスをabstract classにするのは不可能です。


色々面倒ですね。


Scala2.12で、Java8のinterfaceのデフォルトメソッド機能使うようになったら変わるんでしょうか。
あるいは、遠い将来dottyでそもそもASTを保持して、それをもとにコンパイルするようになったら、こんなこと考えなくてすむといいですね。