Scala 3 最新情報

~リリースに向けて準備しよう~ 

自己紹介

  • Taisuke Oe
    • Twitter: @OE_uia
    • GitHub: @taisukeoe
  • フリーランスエンジニア
  • ScalaMatsuri主催してます

 

Scala 3.0.0-M1のリリースが近い!

(Thanks to Dotty Team & Scala Center!)

  • これまでdottyというコードネームで開発が進んでいたもの
  • Scala 3になるというアナウンスは以前からされていた

 

Ref: Dotty becomes Scala 3

Scala 3のmigrataion guideが

整ってきました

(Thanks to Scala Center!)

 

ref: Scala 3 Migration guide · An evolving guide to support the migration to Scala 3

今回の勉強会では

  • Scala 3 - 2.13.x 互換性最新版
  • ライブラリごとの Scala 3 対応状況チェックポイント
  • マルチプロジェクトでの Scala 3 お試し方法

についてお送りします

Scala 3の新機能や、非互換性についての詳細は取り扱いません

Scala 3 - Scala 2.13

互換性最新情報

互換性の軸

  • ソースの互換性
  • コンパイル時のバイナリの互換性
  • マクロの互換性

ソースの互換性

ソースの互換性

コンパイル時のバイナリの互換

両方向について扱える

  • Scala 2.13系から、Scala 3.0のビルド成果物を使える(前方互換性)
  • Scala 3.0から、Scala 2.13系のビルド成果物を使える(後方互換性)

 

なお、Scala 3でビルドすると.jarの他に.tastyというバイナリを生成する。

Scala 3 バイナリを利用するときはjarではなくTASTyを利用することが、互換性維持戦略の前提になっている。

 

ref: Compatibility Reference · Scala 3 Migration guide

コンパイル時のバイナリの前方互換

コンパイル時のバイナリの後方互換

  • 後方 (Scala 2.13 jar <- Scala 3)
    • 依存ライブラリがScala 3.0ビルドされていなくても、自プロジェクトをScala 3にアップデートできる

コンパイル時のバイナリの互換

  • 前方(Scala 2.13 -> Scala 3.0 TASTy)
  • 後方 (Scala 2.13 jar <- Scala 3)

両方向の互換があることで、Scala 3とScala 2.13を混ぜてことができる。

段階的なmigrationが可能になる。

マクロの互換性

  • Scala 3と、Scala 2.13のマクロAPIは全く違う
    • マクロの定義に互換性はない
      • Scala 2.13マクロは、Scala 3版に自力で書き換えなければいけない
      • 自動書き換えも厳しい

マクロの互換性

  • Scala 3と、Scala 2.13のマクロAPIは全く違う
    • マクロの展開も互換性はない
      • Scala 2.13マクロ実装(macro impl)を、Scala 3から直接呼び出して展開することはできない。逆も然り。
      • 『「Scala 2.13のマクロ実装」を呼び出して展開しているScala 2.13のメソッド』をScala 3から呼び出すことはできる。

マクロの互換性

Scala 3 Migration Guideのロードマップ

  • M1 - Dotty書き換えルールのドキュメント化
  • M2 - コミュニティナレッジの移管
  • M3 - 互換性リファレンスの公開 <- イマココ
  • M4 - ScalaCenterパートナー企業によるベータテスト
  • M5 - メタプログラミング・マイグレーションガイドの準備開始
  • M6 - ライブラリ・チュートリアルの公開とテスト
  • M7 - アプリケーション・チュートリアルの公開とテスト
  • M8 - モチベーション・ページの公開
  • M9 - マイグレーションガイドをScala Webサイトに統合

 

ref: Scala 3 Migration Guide (continuation) - Scala 3 Release Projects - Scala Contributors

ライブラリの

Scala 3対応状況

一口に「対応」といっても様々

「対応」=「Scala 3から問題なく使える」と定義すると

  • Scala 3ビルドのartifactが提供されている
  • Scala 2.13のartifactが、Scala 3から利用可能
  • Scala 2.13のartifactが、Scala 3から何らかの理由で利用できない

Scala 3ビルドのartifactが提供されている

Scala 2.13ビルドのartifactが利用可能

  • macroやコード生成を含まないライブラリだと理論上は使える
  • sbt-dottyで withDottyCompat すると、Scala 3からScala 使える
libraryDependencies += ("a" %% "b" % "c").withDottyCompat(scalaVersion.value)

Scala 2.13ビルドのartifactが利用できない

以下のようなケース

  • コード生成される&生成コードがScala 3でコンパイルできない
  • Scala 2.13 macroを含んでいる
  • CompilerPlugin(Scala 2コンパイラ内部に依存している)

できることとしては

  • contributeする
  • 対応を待つ 
  • macro実装の直接の呼び出しはScala 2.13サブプロジェクトでやる

マルチプロジェクトビルド

でどうやってScala 3を取り入れる?

こんなプロジェクトがあったとする

domain

adapter

utils

app

こんなプロジェクトがあったとする

domain

adapter

utils

app

lazy val app = project
  .enablePlugins(PlayScala)
  .dependsOn(adapter, utils)
lazy val adapter = project
  .settings(
    libraryDependencies += 
       "com.typesafe.play" %% "play-json" % "2.9.1"
  ).dependsOn(domain)
val scala213 = "2.13.3"
ThisBuild / scalaVersion = scala213
lazy val domain = project
lazy val utils = project.dependsOn(adapter)

Scala 3導入の順序

  1. Scala 2.13.4に上げる
  2. sbt 1.4.0に上げる
  3. 依存ライブラリのScala 3対応について調べる
  4. 依存グラフの下流からScala 3に上げる

Scala 2.13.4に上げる

sbt 1.4.0に上げる

2.13.4に上げた

domain

adapter

utils

app

lazy val app = project
  .enablePlugins(PlayScala)
  .dependsOn(adapter, utils)
lazy val adapter = project
  .settings(
    libraryDependencies += 
       "com.typesafe.play" %% "play-json" % "2.9.1"
  ).dependsOn(domain)
// 未リリースのためSNAPSHOT
val scala213 = "2.13.4-bin-d66ebf4"
ThisBuild / scalaVersion = scala213
lazy val domain = project
lazy val utils = project.dependsOn(adapter)

ライブラリのScala 3対応状況を調べる

今回のサンプルプロジェクトはPlay利用

domainとutilsだけDotty 0.27.0-RC1へ

domain

adapter

utils

app

lazy val app = project
  .enablePlugins(PlayScala)
  .dependsOn(adapter, utils)
lazy val adapter = project
  .settings(
    libraryDependencies += 
       "com.typesafe.play" %% "play-json" % "2.9.1"
  ).dependsOn(domain)
// 未リリースのためSNAPSHOT
val scala213 = "2.13.4-bin-d66ebf4"
val scala3 = "0.27.0-RC1"
ThisBuild / scalaVersion = scala213
lazy val domain = project
    .settings(scalaVersion := scala3)
lazy val utils = project
    .settings(scalaVersion := scala3)
    .dependsOn(adapter)

各サブプロジェクトの概観

domain (Dotty 0.27.0-RC1)

  • 外部依存が少ない
    • Scala 3にアップデートしやすい
  • (今回はmigrationを推奨するものではないが)幾つかのケースでScala 3の機能が便利?
    • 代数的データ型(ADT)の定義でenum
      • 実態はただのsealed traitとsub typeなので分かりやすい
    • opaque type
      • opaque type Name = String
      • Scala 3系だと別の型だけど、Scala 2系から見るとただのtype aliasとして扱われる(?)ので注意

domainのコード (Dotty 0.27.0-RC1)

package domain

object User {
  opaque type Id = Int
  opaque type Name = String
  object Id {
      def apply(i: Int): Id = i
  }
  object Name {
      def apply(n: String): Name = n
  }
}

case class User(id: User.Id, name: User.Name)

adapter (Scala 2.13.4)

  • play-jsonはmacroを使っている&Scala 3対応してない
    • Scala 2.13の中では使える
  • 現状、Scala 3のサブプロジェクトで定義したデータ型のコンストラクタシグネチャをTASTyで正しく読めない
    • そもそもTASTy readerもScala 2.13.4がWIPなので、現状これがバグなのか仕様(というか限界なのか)判別つかない

adapterのコード (Scala 2.13.4)

package adapter

//some imports here...

object UserFormat {
    // Failed to read User constructor from Scala 3 TASTy. Why?
    // implicit val userFormat: Format[User] = Json.format[User]

    // Explicit read codec works fine. 
    implicit val userReads: Reads[User] = (
        (JsPath \ "id").read[Int] and 
          (JsPath \ "name").read[String]
    )(User.apply _)

    // Watch out: User.unapply signature has been changed.
    implicit val userWrites: Writes[User] = (
        (JsPath \ "id").write[Int] and    
          (JsPath \ "name").write[String]
    )(u => (u.id, u.name))
}

adapterのコード (Scala 2.13.4)

package adapter

import domain._
import play.api.libs.json._

case class Post(user: User, content: String)

object Post {
    import adapter.UserFormat._
    implicit val postFormat: Format[Post] = Json.format[Post]
}

utils(Dotty 0.27.0-RC1)

  • Json変換をplay-jsonに委譲するだけのモジュール
  • Scala 3から、play-jsonを間接的になら利用できる

utilsのコード(Dotty 0.27.0-RC1)

package utils

import domain._
import adapter._
import User._
import UserFormat._
import play.api.libs.json._

object Converter {
    // You can use play-json codec here, since this doesn't call macro expansion directly.
    def toJson(post: Post): JsValue = Json.toJson(post)
    def toJson(user: User): JsValue = Json.toJson(user)
}

app (Scala 2.13.4)

  • PlayFrameworkのcontrollersとviews
    • おおよそplay-scala-seed.g8のまま
  • PlayFrameworkはScala 3に対応していないが、Scala 3で実装したサブプロジェクトに依存可能

appのコード(Scala 2.13.4)

package controllers

//some imports here...

@Singleton
class HomeController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {

  def index = Action { implicit request: Request[AnyContent] =>
    Ok(views.html.index())
  }
  def hello(id: User.Id, name: User.Name) = Action { implicit request: Request[AnyContent] =>
    Ok(Converter.toJson(Post(User(id, name), "Hello, world!")))
  }
}

今回のサンプルコード置き場

まとめ

本日のまとめ

  • Scala 3.0.0-M1が間もなくリリース予定。
  • 今すぐできること
    • Scala 2.13.2以上に上げて、 `-Xsource:3` でコンパイル通す
  • もうすぐできること
    • Scala 2.13.4とsbt 1.4.0が出たら、マルチプロジェクトビルドの一部をScala 3.0に上げてみる。
      • dotcの`-source:3.0-migration`
      • `-rewrite` オプションやScalafixも使えそうなら使ってみる
    • migrationというよりも、Scala 3 お試し&フィードバックのため。

Scala3最新情報

By Taisuke Oe

Scala3最新情報

  • 809