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最新版へのアップデートはどのみち必要になります。今すぐ取り組むことができて、比較的簡単に恩恵を受けることができます。まだの方は今のうちにやっておきましょう。

*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

logica で readable, testable な SQL の夢を見る

こんにちは。河内です。

Logica はデータ操作のための論理プログラミング言語で、Googleオープンソースプロダクトです。 今年の4月に Google Open Source Blog でも紹介されています。

opensource.googleblog.com

Logica のターゲットユーザとして次の3つの人物像が挙げられています。

  • 論理プログラミングをすでにやっていて、より計算力が欲しい人
  • SQLを使っていて、リーダビリティに満足していない人
  • 論理プログラミングを学びたいと思っていて、ビッグデータにそれを適用したい人

この分類でいうと私は2番目の「SQLに満足していない人」です。長いこと付き合ってきていますが、CTEを多数含むある程度長いクエリになると、どうしても読み解くのに時間がかかるんですよね。 logica では readability が上がるということで期待が高まります!

TL;DR;

logica はシンプルな記法を持つ SQL トランスパイラとして使え、次のような特徴があります。

続きを読む

ドメイン駆動設計(DDD)との格闘 - 広義のドメイン・狭義のドメインの理解

こんにちは、株式会社FLINTERSで企画職 (Product Owner、ProductManager、ProjectManager、雑用係などの総称)として働く加藤と申します。 私は、主に、インターネット広告代理店などデジタルマーケティングを実践されている企業へ、Yahoo!GoogleFacebookなどの広告媒体が提供するAPIなどを活用した、業務用のアプリケーションを開発提供するお仕事に携わっております。

引き続きFLINTERSでは、ブログ投稿強化月間です。

企画職としてのドメイン駆動設計(DDD)への参加する時の心得について書いておりますが、第3回となる今回は、ドメイン駆動設計(DDD)における、【ドメイン】という言葉の謎に迫っていきたいと思います。

前回までの振り返り

第1回では、企画職として実践できるようになっておきたい、ドメイン駆動設計(DDD)の手法として4つをあげ、第2回で、ユビキタス言語について述べてきました。

【企画職として実践したいDDD手法】

  1. ユビキタス言語の発見・作成・普及
  2. 広義のドメイン・狭義のドメインの発見・作成
  3. コンテキスト境界の発見・作成
  4. コンテキスト境界同士の関係性の発見・作成

今回は、ドメイン駆動設計(DDD)という名前の通り、鍵となる考え方の【ドメイン】という言葉について理解を掘り下げていきたいと思います。 ※あくまで私個人の考え方なので、「おい!全然間違っているぞ」という部分を見つけた方は優しく教えて下さい

ドメインという言葉が混乱の元

続きを読む

はじめよう GKE Autopilot

こんにちは、清水です。巷で話題の GKE Autopilot について旧来のものと何が違うのか調べ、実際にクラスタを作って挙動を確認してみました。

ちなみに、所属しているチームのプロダクトである CRALY[^1] は社内で最初の Kubernetes クラスタ(GKE)上で構築されたプロダクトです。

  • なにが嬉しいのか
    • ノードとノードプールの管理が不要になり、ワークロードに集中して運用が行えるようになった
    • ノードごとの課金だったのが Pod ごとの課金になった
  • スタンダード環境との違いは
    • リージョンクラスタのみ
    • リリースチャンネルの登録が必要
    • イメージは Containerd を含む Container-Optimized OS のみ
    • ノードとノードプールがフルマネージド
    • マシンタイプは e2 のみ。GPU はサポート外
    • セキュリティ的な観点で、Service の spec.externalIPs は使えない
    • QoS Class は Guaranteed であること(request, limit が同じ)
  • 向いているユースケース
  • Autopilot を試してみる
    • Cluster autoscaler
    • Node auto provisioning(e2-medium でホストできない Pod)
    • Node auto provisioning(ノードセレクタを指定した Pod)
  • さいごに

cloud.google.com

続きを読む

SQL パイプライン開発に便利な Dataform 7つのお気に入りポイント

こんにちは。河内です。 最近はデータ基盤の構築も取り組んでいたりします。

社内では他の DWH が使われている事例がありますが、今回の基盤ではデータソースとの親和性や価格面などを考慮し BigQuery で行くことにしました。 BigQuery 上で多くのデータを順次変換してデータを生成するために何らかのワークフローエンジンが必要でした。

社内の他のシステムではワークフローエンジンとして Digdag を採用している例が多いですが、このシステムでは Kubernetes 上でサービスを運用しているため、当初(2020年12月)は Argo Workflow 上でクエリを順次実行することを構想していました。構想中に DataformGoogle に買収され、無料で使えるようになったというニュースが飛び込んできたため、触って感触が良いことを確かめた後、Dataform を使っていくことにしました。

dataform.co

Dataform は SQL パイプラインを定義し、実行するための環境を提供してくれます。 まだまだ使いこなしているとは言えないかもしれませんが、現時点で私が気に入っている点について書きます。

続きを読む