FLINTERS Engineer's Blog

FLINTERSのエンジニアによる技術ブログ

業務アプリケーションのScala 3アップデートを試してみた(前編)

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

Scala 3.0.0が2021年5月14日にリリースされたことを受けて、いつアップデートするか検討中の方も多いかと思います。

実際、今FLINTERSではScala製プロダクトのScala 3.0.0へのバージョンアップを試みており、今回はその進捗をブログ記事化しました。

TL;DR

Play Frameworkplay-jsonを利用したプロジェクトでも、一部はScala 3.0.0にアップデート出来ることを確認しました。

Scala 3化を完遂するにはやや時期尚早な感があるものの、今回試してみてScala 2.13とScala 3の互換性に関する知見がたまり、OSSへの貢献もできました。

scala3-migrate-pluginで依存しているライブラリのScala 3対応状況について調査したり、社内ハッカソンなどでアップデートに取り組んでみるには良い頃合いではないでしょうか。

本時期の執筆にあたって

Scala 3開発の中心となったEPFLのScala 3 Team、Migration Guideやツールのサポートなどで中心的役割を果たしたScala Center、そしてScala 3.0.0対応に取り組んでいる、Lightbend社を含めた全てのFOSSメンテナ、コントリビュータの皆さんに感謝します。

そもそもの発端

2019/12/18 THE ROAD TO SCALA 3

2019年末記事では、「Scala 3が最もコミュニティから必要としているものは、皆さん一人一人に、ご自身のコードを移植してもらうことなのです。」(訳) *1」と言及されていました。しかし当時は、(以前実施した勉強会でも言及した通り)業務コードを移行するには、試す前に分かる大きな壁がありました。

2020/7/20 Scala 3 Migration Guide(continuation)

2020年夏頃に、Scala CenterからアナウンスされたScala 3 Migration GuideのMilestoneに「Scala CenterパートナーのEarly adapter企業がScala 3へ移行する。(訳)*2」という項目が盛り込まれました。

そろそろScala 3を業務プロジェクトで試しはじめる良い頃合いかな?と思いまして、2021年始め頃からFLINTERSの業務プロジェクトのScala 3移行を少しづつお試ししていました。

現在のステータス

現在FLINTERSでScala 3移行お試し中のプロダクトは、以下のようなサブプロジェクトで構成されています。

root -> application -> domain -> util

依存ライブラリのうち、Scala 3.0.0から利用可能なアーティファクトがpublishされてないものについていえば、rootがPlayScala Pluginを利用しており、applicationがplay-jsonに依存しています。

そのうえで現在のScala 3お試しステータスは、以下の条件付きですがほとんどのテストが通っています。

  • rootを除く、全てのサブプロジェクトのscalaVersionを3.0.0*3へアップデート
  • play-jsonJson.formatマクロを利用した一部テストの除外

説明のため、簡単にしたビルド定義ファイルは以下の通りです。*4

//project/plugins.sbt
addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.8")
addSbtPlugin("ch.epfl.scala" % "sbt-scala3-migrate" % "0.4.4")
// sbt-scala3-migrate 0.4.4が依存するscalafixとsemanticdb-scalacプラグインのversionが古く、Scala 2.13.6をサポートしていないため
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.29")
#project/build.properties
sbt.version=1.5.4
val scala3Commons: Seq[Setting[_]] = Seq(
  scalaVersion := "3.0.0",
  scalacOptions += "-source:3.0-migration",
  libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.9" % Test
)

//build.sbt
lazy val root = project
  .enablePlugins(PlayScala)
  .settings(
    scalaVersion := "2.13.6",
    // sbt-scala3-migrate 0.4.4が依存するscalafixとsemanticdb-scalacプラグインのversionが古く、Scala 2.13.6をサポートしていないため
    semanticdbVersion := scalafixSemanticdb.revision,
    scalacOptions ++= Seq("-Ytasty-reader", "-Xsource:3", "-deprecation"),
    // 同一ライブラリの、Scala 3向けとScala 2.13向けアーティファクトを混ぜることはできないため、
    // Test/applicationに依存する場合は3向けアーティファクトを除く
    // libraryDependencies -= "org.scalatest" % "scalatest_3" % "3.2.9" % Test,
    libraryDependencies ++= Seq(
      "org.scalatest" %% "scalatest" % "3.2.9" % Test,
      "org.scalatestplus.play" %% "scalatestplus-play" % "5.1.0" % Test
    )
  )
  .dependsOn(application)

lazy val application = project
  .settings(scala3Commons)
  .settings(
    libraryDependencies ++= Seq(
      // `publishLocal` したplay-jsonのversion
      "com.typesafe.play" %% "play-json" % "2.9.2+118-f1769bd5-SNAPSHOT",
      ("com.typesafe.akka" %% "akka-stream" % "2.6.14")
        .cross(CrossVersion.for3Use2_13)
    )
  )
  .dependsOn(domain)

lazy val domain = project
  .settings(scala3Commons)
  .dependsOn(util)

lazy val util = project.settings(scala3Commons)

今回マイグレーションのためにやったこと

以下で、Scala 3 マイグレーションのためにやったことについて紹介します。

Scala 3 Migration Guideを読む

scala-lang公式サイトにて、Scala 3 Migration Guide が公開されています。

マイグレーションのために知っておくべき基礎知識がカバーされています。まずはざっとでもいいので、一読しましょう。ただし掲載されているScalaやsbt-plugin等のversionが最新ではない場合がありますので、そのときの最新版を使うとよいでしょう。

また、Scala 3 migrate pluginというsbt pluginを入れると、依存するライブラリのScala 3サポート状況や、互換性のビルド定義の変更方法の提示、ソースコードに含まれるScala 3非互換の構文のうち一部をScalafix ruleで修正する機能など、migrationに役立つ様々な機能が入っています。*5

Scala, sbt, Play Frameworkやその他ライブラリを、できるだけアップデートしておく

Scala 3開発に際し、TASTyという中間コードフォーマットが新規に開発されました。ファイル拡張子は .tastyで、Scala 3向けにビルドされたjarファイルの中に含まれ、配布されます。

このTASTyにより、Scala 3.0.0からScala 2.13.6、Scala 2.13.6からScala 3.0.0双方向について、マクロやScala 3の一部の新機能を除いたバイナリの互換性を達成しています。

このバイナリ互換性を実現するため、sbt(執筆時点で最新は1.5.4)は以下のような特別なハンドリングをしてくれるようになりました。

  • scalaVersionが "3.0.0" からはじまるときに、scala-libraryのartifactIdをscala3-libraryにして解決してくれる
  • scalaVersionが異なる(3.0.0と2.13.6など)サブプロジェクト間で依存(dependsOn)できる
  • Scala 3とScala 2.13の間で、バイナリを相互利用するためのイディオムが導入される
    • .cross(CrossVersion.for3Use2_13) を付与すると、Scala 3サブプロジェクトからでも、Scala 2.13のjarを解決してくれる

なおPlay Frameworkは以下のissueにより、Play 2.8.8以外はsbt 1.5.0以上でコンパイルすることができません。よってsbtを1.5.4に上げるには、Playも2.8.8に上げる必要があります。

https://github.com/sbt/sbt/issues/6400

以上の理由により、Scala 2.13.6, sbt 1.5.4, Play Framework 2.8.8にそれぞれアップデートすることは、Scala 3対応のためにも特に重要です。

その他のライブラリについても、Scala 3向けアーティファクトは最新のversionについてのみ提供されるケースがほとんどなので、あらかじめ最新近くまでアップデートしておくと良いでしょう。

サブプロジェクトの依存グラフの下流から、Scala 3にアップデートする

もともとScala 3.0.0はScala 2.13の構文の大部分は(警告が出るとしても)可能な限りそのままコンパイルが通るように設計されています。またScala 3には "-source:3.0-migration" というscalac optionが用意されており、このoptionをつけるとマイグレーションモードになって非互換な構文のほとんどはコンパイルエラーではなく警告対象となります。

そのため、業務プロジェクトのScala 3アップデートでコンパイルやテストを通すことを目指すなら、構文の非互換性よりもライブラリの対応状況の方がブロッカーになることが多いでしょう。つまり依存しているライブラリが少ないサブプロジェクトほど、アップデートが容易であると期待できます。

本プロダクトのサブプロジェクトの依存グラフは、以下の通りです。

(上流) root -> application -> domain -> util (下流)

そこで、サブプロジェクトの依存グラフの下流であるutilから、以下のように順番にScala 3へアップデートし、テストを通していきました。

サブプロジェクトごとに、依存ライブラリのScala 3対応状況について調べる

依存ライブラリのScala 3対応状況次第で、以下のような対応をしていきました。

  1. Scala 3対応のアーティファクトがある場合はそれを使う
  2. Scala 2.13対応アーティファクトがマクロを利用しており、Scala 3対応はmain branchで完了しているもののpublishされてないときは、local publishして使う
  3. Scala 2.13対応アーティファクトがマクロを利用していない場合は .cross(CrossVersion.for3Use2_13) を指定して使う
  4. Scala 2.13対応アーティファクトがマクロを利用しており、Scala 3対応が完了していない場合は、依存サブプロジェクトをScala 2.13のままにする。

Scala 3対応アーティファクトの有無、マクロの利用有無については、今はscala3-migrateプラグインmigrate-libs コマンドを利用するのが最も簡単だと思います。

sbt> migrate-libs application
[info] com.typesafe.play:play-json:2.9.2                   -> X : Contains Macros and is not yet published for Scala 3.
[info] com.typesafe.akka:akka-stream:2.6.14              -> "com.typesafe.akka" %% "akka-stream" % "2.6.14" cross CrossVersion.for3Use2_13 : It's only safe to use the 2.13 version if it's inside an application.
[info] "com.typesafe" % "config" % "1.3.3"               > Valid : Java libraries are compatible.

play-jsonの対応を保留する

play-jsonは、執筆時点でScala 3向けアーティファクトがpublishされていませんが、mainブランチはScala 3に対応済です。

ただし最新のmainブランチをlocal publishしたとしても、今回のようにScala 3と2.13を混ぜたプロジェクトでは Json.format を利用しづらい状態です。例えばplay-jsonJson.format 実装を利用したコードがあるとして:

case class A(a: String)
implicit val formatA: Format[A] = Json.format[A]

このA、formatA、そしてformatAを利用するコードがScala 3 / Scala 2.13の境界をまたぐと、以下の2つの理由により殆どのケースでコンパイルエラーに遭遇します。*6

  • Scala 3のcase classのコンパニオンオブジェクトのunapplyメソッドのシグネチャScala 2.13と異なること
  • Scala 2.13のJson.formatのマクロ実装が、コンパニオンオブジェクトのunapplyメソッドを利用すること
  • Scala 3マクロをScala 2.13から直接呼び出す、もしくはその逆はできないこと

もちろんJson.formatを利用しないコードに書きかえればテストを通すことも可能ですが、そもそも全サブプロジェクトをScala 3にアップデートすれば解決できる問題です。

今回は???-Yignore-scala2-macros*7などを利用してコンパイルだけ通し、テストを対応させることは保留しました。

マイグレーションモードでもコンパイルエラーとなる非互換の構文を直す

今回取り組んだプロジェクトでは、パッケージ名やメソッド名の一部に export という単語が使われていました。

Scala 3では export は予約語になっており、マイグレーションモードであってもコンパイルエラーとなりますので、いったん exports にrenameしました。

依存しているライブラリのバグを踏んだら報告する、もしくは直す

Scala 3.0.0へアップデートする際、Scalaやライブラリのバグに遭遇することもまだまだ多いです。

今回はScalaについていえば、-Ytasty-reader絡みの複数のバグ*8*9に遭遇したため、それぞれワークアラウンドのコードを追加しました。

また、ScalaTestのDiagramsのマクロ関連バグに遭遇し、以下のように報告や修正を行いました。修正版はまだpublishされていないため、今は with Diagramsコメントアウトしています。

github.com

github.com

総括

現在FLINTERSでは、一部の業務プロダクトのScala 3アップデートを試みていますが、道半ばです。

Scala 3へのmigrationは、そのプロジェクト依存しているライブラリやフレームワークによって難易度が変わりますが、総じて未解決のバグに遭遇することも多い状態だと思います。

今回のプロダクトのようなPlay Frameworkやplay-jsonを利用したプロジェクトでは、Scala 3.0.0移行を部分的にでも完遂するには、時期尚早な感がありました。しかしながら、今回migrationを試したことでScala 2.13 <-> Scala 3の互換性に関する知見がたまり、また幾つかのOSSへの貢献につながりました。

Scala 3.0.0-M1当初から比べてマイグレーションガイドやツールも充実してきましたので、scala3-migrate-pluginで依存ライブラリの対応状況を調査したり、社内ハッカソンなどでアップデートに取り組んでみるには丁度よい頃合いではないでしょうか。

そしてScala 3へアップデートするためには、Scala 2系、sbt、そしてPlay最新版へのアップデートはどのみち必要になります。今すぐ取り組むことができて、比較的簡単に恩恵を受けることができます。まだの方は今のうちにやっておきましょう。

2022/4/11追記: 後編のお知らせ

labs.septeni.co.jp

*1:原文: "The biggest thing Scala 3 needs from the community is for everyone to begin porting their code."

*2:原文: "M4 - Early adopter company partnered with Scala Center" "The Scala Center will partner with companies that are willing to migrate to Scala 3."

*3:執筆時点の最新の3.0.1-RC2についても試しています

*4:実際には他にも様々なライブラリやpluginが入っていますが、今回のScala 3 migrationにはあまり関係ないため割愛します。

*5:今回のブログ執筆にあたっては、Scala 3 migrate pluginの開発が進む前に色々試してしまったので、実は自分の場合はあまり活用する機会に恵まれませんでした。しかし、これからScala 3 migrationを試す人にとっては大きな助けになると思います。

*6:例外として、Scala 2.13に定義されたクラスAに対して、Scala 3側でformatAを定義し、かつ利用するパターンなら使えます。

*7:

*8:https://github.com/scala/bug/issues/12369

*9:https://github.com/scala/bug/issues/12409