FLINTERS Engineer's Blog

FLINTERSのエンジニアが綴る技術ブログ

業務アプリケーションをScala 3で動かしてみた(後編)

こんにちは。FLINTERSでTech Adviserをしています、OE(@OE_uia)です。

Scala 3.0.0が2021年5月14日にリリースされたことを受けて、いつアップデートするか検討中の方も多いかと思います。 実際、今FLINTERSではScala製プロダクトのScala 3へのバージョンアップを試みています。前回の「業務アプリケーションのScala 3アップデートを試してみた(前編)」の記事では、主にScala 3マイグレーションに役立つ情報や機能についてご紹介しました。

labs.septeni.co.jp

今回はその後編です。

TL;DR / まとめ

Play Frameworkを利用している業務アプリケーション CRALYにおいて、Scala 3でメインのコンパイルが通り、かつローカル環境のdev modeで「一応」動きました。本プロジェクトのScala 3アップデートの展望はひらけてた、といえると思います。

一方で、複数の依存ライブラリをバイナリ互換性が保証されてないバージョンのjarで上書きしていますので、どこかで実行時例外が投げられる恐れが残っています。しかもScala 3未対応の、マクロを含むテストライブラリに依存しているため、appレイヤーの自動テストが通っていません。結果として、プロダクションにはデプロイできない状態です。

Scala 3とScala 2.13を混在させてビルドするための種々の機能は、Scala 3へのアップデートの過程で局所的に使うには便利ですし、今回の取り組みでもScala 2.13でビルドされたjarが多数残っています。一方で、Scala 3でビルドされているものと、Scala 2.13でビルドされているもの*1を長期間混ぜて使い続けるのは、あまりお勧めできるものではないということも分かってきました。

これらの機能はほとんどの場合うまく動きますが、未解決のバグ踏んでむしろコンパイルエラーが増えることもありました*2。またバグに遭遇せずとも、後述の菱形依存性問題は避けられません。

皆さんの業務プロジェクトをScala 3にアップデートする際には、Scala 3とScala 2.13が混在する範囲や期間を小さくするよう努めることをオススメします。

依存ライブラリのScala 3の対応状況

2022年3月、Scala 3のマクロで実装しなおされたplay-jsonの2.10.0-RC6がリリースされました。

これによってメインが依存しているライブラリのうち、Scala 3未対応かつ利用できないものは scala-guiceだけになりました。 Scala-guiceScala 2のTypeTagを利用していることがネックで、2022/3/29現在、Scala 3から利用することができません。これはGuiceをそのまま使うことで回避しました。

なお、scalaVersion は 3.1.2-RC2 です。

//build.sbtより一部改変・抜粋
    libraryDependencies += "com.typesafe.play" %% "play-json"   % "2.10.0-RC6",
    libraryDependencies := libraryDependencies.value.map {
      case module if module.organization == "com.typesafe.play" && module.name != "play-json" => 
        module.cross(CrossVersion.for3Use2_13)
      case other => other
    }
    .map(
        _.exclude( "com.typesafe.play" , "play-json_2.13")
    ),
    // Thanks to https://xuwei-k.github.io/slides/alp-scala-3/#20
    conflictWarning :=  ConflictWarning("warn", Level.Warn, false),

上記の通りPlay Framework由来のjarは、play-json以外2.13版を利用しています。

Scala 3対応を進める上でハマった罠

思い起こされるのものを列挙します。

これらの全てが、Scala 2.13でビルドされるものと、Scala 3でビルドされるものを混在させたことに起因していました。

Scala 2.13と3が混在できる理由

Scala 2.13とScala 3のコンパイラは、以下のような双方向の互換性を実現しています。

  • Scala 3コンパイラは、Scala 2.13コンパイラが生成したクラスファイルを読むことができます。
    • ただしScala 2系のマクロや、削除されたearly initializerなど、Scala 3から利用できない機能が存在します。
  • Scala 2.13.4~ コンパイラはTasty readerを内蔵しており、 -Ytasty-reader オプションをつけることでScala 3の中間コードフォーマット Tasty を読むことができます。
    • ただしScala 3の一部の新機能など、Scala 2.13から利用できないものが存在します。

これらに加え、sbtのマルチプロジェクトビルドでは、Scala 2.13とScala 3のサブプロジェクトを混在させることができます。

Scala 2.13と3を混在させる理由

Scala 2.13でビルドされたライブラリが混在する理由は、Scala 3版のjarがまだpublishされていないライブラリが少なくないからです。

Scala 3対応が完了していない主因は、マクロを利用するライブラリのアップデートコストが高いことです。一方で、メンテナンスしていた企業の開発体制変更などにより、開発リソースがあまり割かれなくなったPlay Frameworkのようなケース*3もあります。

Scala 2.13のサブプロジェクトが混在する理由は、Scala 3アップデートの最中に、Scala 2.13のサブプロジェクトが残ったままマルチプロジェクトビルド全体のコンパイルを通したいときがあるからです。Scala 3非互換の構文や機能が残るコンポーネントScala 3対応を後回しにしたいケース、Scala 3対応の過程でpublicなインターフェースを変更する必要がある場合などでは、全体のコンパイルを先に試す方が便利でしょう。今回の取り組みでいえば、プロジェクト全体から export というScala 3の新予約語を除く際に活躍しました。

菱形依存性問題

以下のような依存関係のモジュールA, B, C, Dにおいて、BとCがDのバージョン(それぞれD-b, D-cとします *4 )が異なるケースを考えます。このとき、AにとってDは推移的な依存 transitive dependency であるといいます。

f:id:oe_uia:20220402205443p:plain

D-bとD-cにバイナリ互換性が保証されていれば、ビルドツールの依存性リゾルバーが適切なバージョンを選択することで問題にはなりません。

しかしD-b, D-cにバイナリ互換性が保証されていない場合、安全に解決することができません。これは菱形依存性問題 diamond dependency problem と呼ばれています。

菱形依存性問題に遭遇した場合、以下のいずれかのアクションを行う必要があります。

  • 直接依存するBもしくはCのversionを変えるなどして、依存するDのバージョン(D-b, D-c)がバイナリ互換な組み合わせにする
    • もし難しい場合は、B,Cに最新のDに依存させるPull Requestを出し、jarをpublishしてもらう
  • (大きな手間なく可能ならば)BもしくはCへの依存を止める

菱形依存性問題はScala 3に限った話ではなく、ライブラリ向けライブラリのBreaking Changeのたびに発生しがちです。*5

以上をふまえた上で、 「Xについて菱形依存性問題が起きる」という書き方をした場合、Xは上記の菱形図の中でいうDのポジションであるとします。

今回遭遇した菱形依存性問題

一例として、scala-parser-combinatorsについて菱形依存性問題が起きたケースを挙げます。

f:id:oe_uia:20220402205443p:plain

  • A: 本アプリケーション
  • B: play_2.13
  • C: scalikejdbc-core_3
  • D: scala-parser-combinators

play_2.13 v2.8.14は、scala-parser-combinators_2.13 v1.1.2 (先ほどの図でいう D-b)に依存しています。

一方で、scalikejdbc-core_3 v4.0.0は、scala-parser-combinators_3` v2.1.0 (先ほどの図でいう D-c)に依存しています。

v1.1.2v2.1.0 の間にはバイナリ互換性は保証されていませんので、scala-parser-combinators において菱形依存性問題が起きます。今回の取り組みにおいては、前述の通り安全ではありませんが scala-parser-combinators_3 v2.1.0 を利用することにしました。

Scala 3と菱形依存性問題

Scala 3の場合、Scala 2.13のjarを利用することができる(むしろ利用しなければならない)一方で、同じライブラリ*6Scala 3版jarと、Scala 2.13版jarが混在する状態では(明示的に警告レベルを下げたり、excludeしない限り)ビルドできません。また先の scala-parser-combinators のように多くのライブラリにおいて、Scala 3対応版は新しいメジャーバージョンがつけられてリリースされています。結果として、推移的な依存ライブラリ(先の図でいうD)のScala 2.13版jarとScala 3版jarが混在してしまった場合、それらの間のバイナリ互換性はすべて保証されていない組み合わせでした。

よしんば推移的な依存ライブラリが同じrevisionだったとしても、Scala 2.13でビルドされたjarとScala 3でビルドされたjarを、アプリケーションの再ビルドなしに安全に交換可能してよいか?(バイナリ互換性が保証されてるか?)でいえば、少なくとも現状はバイナリ互換性が保証されていないものとして扱う方がよいと思います。Scala 2.13やScala 3特有の機能*7を除く限りにおいて、できるだけScala 2.13とScala 3の間でバイナリ互換になるように配慮されている*8ようですが、まだバイナリ互換性を破壊するバグも存在しているようです*9*10

以上の理由により、ライブラリ依存性の中でScala 2.13版とScala 3版が混在すると、菱形依存性問題に遭遇しやすくなります。

逆に全てのライブラリのScala 3対応が完了してしまえば、Scala 3はバイナリ互換性を改善していますので*11、むしろScala 2系より菱形依存性問題の発生頻度は下がるはずです。

最後に

これまで見てきたように、Scala 3 <-> Scala 2.13の相互運用性には落とし穴が幾つかありますし、ストレスなく相互運用して使えることは期待しない方がよいでしょう。

しかし逆にいうと、多少のワークアラウンドを許容しさえすれば、異なるメジャーバージョンの言語やアーティファクトを混ぜてビルドすることができるのです。これは今までの常識が覆った、驚きの体験でした。

Scala 3へのマイグレーションを支えている、全てのOSSハッカーに感謝します。

*1:後述のScala 2.13標準ライブラリは、例外と思う方がよいかもしれません?

*2:もちろん、積極的に使い倒してバグを報告する、もしくは修正するというのはよいOSS貢献にもなります。

*3:Play Frameworkは 現在Lightbend社からOpen Collectiveへ移管され、このOpen Collective上で開発原資となる寄付を$1から募っています。Play Frameworkに寄付をすることは、メンテナンスを継続するという意味でも、Scala 3対応を早めるという意味でも、重要な貢献手段になりました

*4:このD-b, D-cでいうバージョンとは、organizationId % artifactId % revision でいうところの revision だけではなく、ビルドしたScalaバイナリバージョン由来の 2.13, 3 というartifactIdのsuffiixも含めることとします。

*5:例えばCatsのメジャーアップデートの際にも発生しており、CatsのREADMEにもその旨の注意書きがあるのを、目にしたことがある方も多いでしょう。

*6:同じorganizationIdと、Scalaバイナリバージョンを除いて同じartifactIdを持つもの

*7:もちろん、scala3-library.jarへの依存という差もあります。

*8: Binary versions in the Scala 3.x era · Issue #10244 · lampepfl/dotty · GitHub

*9: 一例ですがSpurious cyclic reference error · Issue #13937 · lampepfl/dotty · GitHub はバイナリ互換性を破壊している、と主張されています

*10:

*11:現時点でバイナリ後方互換性が保証されています。すなわちScala 3.1.xからは、Scala 3.0系でビルドされたjarを安全に利用できます。一方、バイナリ前方互換性はScala 3.1では保証されていませんが、現在改善の議論と取り組みの真っ最中です。