Scalaのversion間の非互換について具体的に考える

わりと以前から書いてみたかったことなので、書いてみます。


たとえば最近ではここ
Web✕Java - HTML5で進化したWeb標準を、Java技術でどう扱うのか?でStruts使ってる人へJSFの説明をしてきた #jjug #html5biz
でとりあげられていたりします。*1
昔からずっと言われてる話です。ですが、「互換性がない」とある程度多くの人が口をそろえて言う一方、「ではどのくらいの互換性があればいいのか(どういうポリシー?どのくらいの期間を保証?)」という具体的な議論がほとんどされていない気がします。
なので、もうちょっと具体的に考えてみたいと思います。


先日こんなtweetをしました

Scalaを始めてあまり日数が経っていない初心者と、ある程度の経験者(Scala2.8〜Scala2.10と複数のversionを経験してる)では、このあたりの印象や理解が結構異なる気がします。


まず、現在のScala本体のversion間の互換性についてまとめてみましょう。

大きく言うとメジャーバージョンアップが、1年〜2年に一回あるという感じです。ここでのメジャーバージョンアップとは、Scala2.8からScala2.9、Scala2.9からScala2.10というバージョンアップのことです。
そして、主にバグフィックスを目的*2としたマイナーバージョンアップがあります。これは、随時出すので*3あまりはっきり決まっていませんが、たとえば

  • 2.8は2.8.0、2.8.1、2.8.2
  • 2.9は2.9.0、2.9.0-1、2.9.1、2.9.2、2.9.3
  • 2.10は2.10.0、2.10.1、2.10.2、(もうすぐ2.10.3がでる)2.10.3、2.10.4まで出た

という感じです。Scala2.8以降のここ数年は必ずこのようなversion番号の規則です。*4このマイナーバージョンアップでは、バイナリ互換性が保証されています。*5


さて、互換性がないのは、メジャーバージョンアップです。バイナリ互換がないので、異なるバージョン(例えば2.9と2.10)は混ぜて使うことができません。また、ソースコード互換があっても、かならずコンパイル自体をやり直さないといけません。


さてそうすると、具体的にどのような状況になるか(現在どのような状況なのか?)という話です。
まず、上記の「メジャーバージョンアップの非互換が絶対に受け入れられない人」はここでは論外・・・(というとなんだか言葉が過激に聞こえますが)少なくとも現状ではScalaを使うべきではありません。

ここで最初にあげた自分のtweetに戻るのですが、ライブラリ作成者や、Scala全体のエコシステムを考えてみることが重要です。
先ほどメジャーバージョンアップと、マイナーバージョンアップの話をしましたが、Scala本体のメジャーバージョンアップ間のソースコード互換性の話をしていないので、まずその話もしましょう。おおまかに言うとメジャーバージョンアップ間のソースコード互換は

  • いきなり消すことはせずに、まずdeprecatedアノテーションがつけられる
  • deprecatedアノテーションがつけられた更に"次"もしくは"次の次"のversionで実際に削除される

ということが大半です。たとえば、scala.Applicationというtraitは、Scala2.9でdeprecatedになって、Scala2.10でもdeprecatedのまま存在し、2.11では実際に削除されます*6
また、上記のようにdeprecatedをつけて順調に消せる場合は良い方ですが、どうしても細かい仕様変更などによって、いきなりソースコード非互換が出る場合もあります。


ここまでの話から結論づけると
Scalaライブラリ本体のみに依存するライブラリを作成する場合、3世代のクロスビルドが限界」という話です。いきなりクロスビルドという言葉を出しましたが、先ほど話したようにバイナリ互換がないゆえに、Scalaのライブラリ作成者は複数のversion(たとえば2.9と2.10)のjarをそれぞれ作成してpublishするということをやっています。sbtをつかってこんな感じ https://github.com/scalaz/scalaz/blob/v7.1.0-M2/project/build.scala#L50 で設定をします。

さて、いま「Scalaライブラリ本体のみに依存」とあえて最初につけましたが、大抵のライブラリは、他のライブラリに依存しているものです。そうすると「多くのライブラリに依存してるライブラリ」は「Scala本体のみに依存してるライブラリ」よりも、依存の多さから、クロスビルドが難しくなってくるのが必然でしょう*7

よって、実際現状のScalaライブラリを見渡すと
「2世代のみクロスビルドしている場合が大半」
です。2世代とは具体的にいうと「Scala2.8のサポートは打ち切り、Scala2.9とScala2.10のみ」ということです。

統計をとったわけではなく、自分の感覚でいうと、6割〜8割が2世代のみサポート、1世代のみサポートもしくは「頑張って3世代サポート」が、それぞれ残り1割〜2割というところでしょうか。*8


先ほど、Scala本体のメジャーバージョンアップにおいては、「deprecatedアノテーションをつけて、次もしくは次の次に実際に削除」という方針を紹介しましたが、Scala本体以外のライブラリでそこまで丁寧にやってる例は稀です。そこの方針はライブラリ毎にバラバラです。


さてだいぶ長くなってきて、なにがいいたいのかわからなくなってきましたが、
Scala本体の互換性のみではなく、その影響をうけるScalaエコシステム全体のサイクル」
を考える必要がある、ということです。

仮に、Scala本体のみに依存してアプリケーションをつくるならば

  • Scala本体のメジャーバージョンアップは1〜2年に一回
  • ちゃんとdeprecatedになって、次、または"次の次"に削除される

ということから、3〜4年経っても、コンパイルをやり直す手間がかかるだけで、ソースコードはほぼ変更なしか、少しだけ変更する*9だけで済むでしょう。

しかし、色々なライブラリに依存してるアプリケーションを3〜4年放ったらかしておいて、いざアップデートしようとした場合には、「少しだけの変更」で済むはずはありません。個人的感覚では、実際Scalaでつくったアプリケーションを長期メンテするなら、2年くらい(?)のある程度短いサイクルでScala自体や依存ライブラリのバージョンアップ上げる作業をしないと、つらいと思います。


つまり
Scala本体だけの互換性が上がっても、Scala全体のエコシステムの互換性が向上しないと意味が無い」

ということです。*10逆に言うと、もしかすると

Scala本体の互換性がこのままでも、Scala全体のエコシステムの互換性を向上させることは可能かもしれない」

ということです。*11


ここまでで言いたいことは、だいたい言い終えて、べつにこれ以上自分が具体的な解決策を示せるわけではありません。ただ、まず

  • 具体的にバージョン非互換とはどういうことなのか?
  • そのバージョン非互換はどういう原因でそうなってるのか?

ということを知ってもらいたかった、ということでしょうか。まずそこを知ってもらわないことには、「どうやってバージョン互換を向上させていくのか?」の議論すらできません。

そもそも、それぞれ

  • 現状の互換性で十分だし、互換性守るよりも、どんどん新機能開発して欲しい
  • この程度の互換性では、全くScalaを使う気にならない

と色々な人がいるはずです。それはまた、「Scala本体に対して」と「それぞれのライブラリに対して」それぞれあります。


ちなみに、Scala本体は、観測している限り、バイナリ互換やソースコード互換のポリシーはそれほど変える気はなさそうなので、(少しずつ成熟して緩やかにはなるでしょうが)しばらくこの程度の互換性の状況は続くと思います。


さて、5年後、10年後にはどうなっているんでしょうかね・・・。

*1:念のためいっておくと、この記事責めたいわけではまったくなく、とても正しいことを言ってると思います

*2:ちょっとだけ互換性ある新機能が入る場合もある

*3:どのくらいの期間でだすのか、何回だすのか、などは

*4:ちなみに、Scala2.7以前はこのversionの規則に従いません

*5:ソースコード互換もほぼ100%保証されているはずですが、極稀にバグなどにより崩れたりしてます・・・。

*6:まだ2.11のfinalはリリースされていませんが、すでに消されています

*7:ほかの言語詳しくないですが、このあたりの感覚は他の言語のライブラリでも同じではないでしょうか?

*8:たとえば、playframeworkは、2.2系まではクロスビルドせずScala2.10の1世代のみです

*9:しかも、ちゃんとdeprecatedアノテーションがついているはずなので、変更方法がわかりやすい

*10:もちろん、ある程度の相関はあるでしょうけど

*11:ただ、現状ライブラリ作成者はみんなできる限り頑張ってクロスビルドしてるので、そう簡単に向上するものではありませんが。