Opt Technologies Magazine

オプトテクノロジーズ 公式Webマガジン

Introduction to Scala 3

f:id:opttechnologies2015:20190925175337p:plain

先日のScala Days 2019の基調講演などでも触れられた通り、いよいよDottyあらためScala 3のリリースが近づいて来ました。既にScala 3の仕様のかなりの部分は、Dottyとしてユーザが利用できる状態になっています。この記事では、Scala Daysでの講演スライドおよびDottyに関する各種資料を元に、Scala 3の新機能や変更点について紹介します。

あいさつ

こんにちは。@kmizuです。7月に入社したばかりで、まだ確固とした役割は定まっていませんが、教育に開発に研究にと色々頑張ろうと意欲を燃やしている今日この頃です。

今回は、一人のScalaウォッチャーとして、Scala 3 (Dotty)についての全体像をお伝えできればと思います。

Scala 3について

現在利用されているScalaのバージョンは、Scala 2.11、Scala 2.12、Scala 2.13といったものですが、これらを総称してScala 2やScala 2系、あるいは単にScalaと呼びます。実際には、Scala 2の中でも言語仕様の変更はあったので、Scala 2系とくくることには多少語弊がありますが、ともあれ、Scala 2の基盤となる共通の言語仕様があるのは確かです。Scala 3は、Scala 2での反省をもとに様々な改善(機能の追加や削除を含む)を行った新しいバージョンのScalaです。

Dottyとは

Dottyは、Scalaの設計者である、スイス連邦工科大学ローザンヌ校(EPFL)のMartin Odersky教授らが中心となって開発した、Scala 3の機能を実装している処理系です。コードベースは以下にあります。

https://github.com/lampepfl/dotty

Dottyのコードベースは、現在のScala 2のコードベースである以下とは別であることに注意してください。

https://github.com/scala/scala

Dottyは、コンパイル速度の改善や新機能の実装など様々な変更点を含むため、現在のScala 2のコードとは別にフルスクラッチで書き直されています。処理系がフルスクラッチで書き直されたといっても、Scala 2のコードの多くはコンパイルを通るようになっています。

今回紹介する機能のほとんどは、Dotty 0.18.1-RC1で動作確認をしています。ただし、詳細な構文についてはScala 3の仕様としてScala Improvement Process (SIP)で確定するまでは変更の可能性があるので注意してください。

Scala 3の新機能

Scala 3では、Scala 2で実現可能なものの、冗長な形でしか書けなかったものがより簡単に書けるようになっています。この節では、Scala 3の新機能のいくつかについて紹介していきたいと思います。

Top level definitions

現在のScalaでは、ファイルのトップレベルには、クラス(class)、トレイト(trait)、オブジェクト(object)の宣言のみが可能です。もし、単なるユーティリティメソッドが必要な場合でも、次のように、objectでくるんであげる必要があります。

object MyMath {
  val Pi: Double = 3.141592653589793
  def plus(x: Int, y: Int): Int = x + y
}
object Usage {
  def main(args: Array[String]): Unit = {
    println(MyMath.plus(1, 2)) // 3
    println(MyMath.Pi)
  }
}

Scala 3では、トップレベルにメソッド(def)や値(val)の宣言を置くことができるようになりました。たとえば、上のコードは以下のように書くことができます。

val Pi: Double = 3.141592653589793

def plus(x: Int, y: Int): Int = x + y

def main(args: Args: Array[String]): Unit = {
  println(plus(1, 2)) // 3
  println(Pi)
}

トップレベルにはクラスとトレイトとオブジェクトしか宣言できないというのはシンプルではありましたが、一方で、不便な点でもありました。トップレベルにメソッドや値を定義できるのは、それまでよりシンプルではないものの、便利でもあります。

Creator applications

現在のScalaでは、オブジェクトを生成するためには原則としてnew ClassNameとする必要があります。case classを定義した場合にはnewを省略できるように見えますが、実態としては、apply()メソッドが自動的に定義されるためです。Scala 3では、全てのクラスについて、生成するときにnewを省略できるようにしました。

class Point(val x: Int, val y: Int)
def main(args: Array[String]): Unit = {
  val p = Point(1, 2)
  println(p.x) // 1
  println(p.y) // 2
}

個人的には、原則としてnewが必ず必要というアプローチの方が好みではあるのですが、初学者にとっては、case classで定義したクラスのインスタンスを生成するときと見た目が同じになるので、学ぶのが楽になると言えそうな気もします。

Multiversal equalities

現在のScalaでは、異なる型であっても、必ず==で比較できます。たとえば、以下のコードは必ずfalseになりますが、警告も出ません。

object Usage {
  def main(args: Array[String]): Unit = {
    val x = 1.5
    val y = 1
    println(x == y) // false
  }
}

このような挙動のせいで、異なる型のオブジェクトを誤って == で比較した結果、バグの原因になることがありました。

Scala 3では、ユーザーが定義した型に対して、derives Eqlをつけることで、異なる型同士の比較を防ぐことができます。

case class A(v: Int) derives Eql
case class B(v: Double) derives Eql
def main(args: Array[String]): Unit = {
  A(1) == B(1.5)
}

このコードをコンパイルしようとすると、以下のようなエラーが出ます。

A(1) == B(1.5)
^^^^^^^^^^^^^^
Values of types A and B cannot be compared with == or !=

現在のScalaでも、サードパーティのライブラリによって、このようなことを保証することは可能ですが、組み込みの==!=に対してはそのような性質を保証することができませんでした。コードの安全性を高めるという点でより良い変更と言えると思います。

Extension methods

C#やKotlinには、extension method (拡張メソッド)と呼ばれる機能があり、既存のクラスにメソッドを追加したかのように見せかける記述が可能です。Scalaでは、言語機能としての拡張メソッドのサポートはないものの、以下のようにすることで、同じ効果を実現することができます(enrich my libraryパターン)。

object Extension {
  implicit class RichDouble(self: Double) {
    // selfの2乗を計算するメソッド
    def square: Double = self * self
  }
}
object Usage {
  import Extension._
  def main(args: Array[String]): Unit = {
    println((2.0).square) // 4.0
  }
}

Scala 3では、この機能が言語標準の構文としてサポートされており、以下のように記述することができます。

def (self: Double) square: Double = self * self
def main(args: Array[String]): Unit = {
  println((2.0).square) // 4.0
}

現在のScalaと比較すると、構文が簡潔になったのと、拡張メソッドのために新しい名前を定義する必要がなくなったのがわかります。なお、拡張メソッドを定義するための構文については、今後まだ変更の可能性があるため、注意してください。

Enums

現在のScalaでは、いわゆる列挙型や代数的データ型(Algebraic Data Type)を実現するために冗長な記述が必要です。

たとえば、RedGreenBlueを表すColor型は、Scalaでは以下のように記述する必要があります(Dottyのページより引用)。

sealed asbstract class Color
object Color {
  case class Red extends Color
  case class Green extends Color
  case class Blue extends Color
}

object Usage {
  def main(args: Array[String]): Unit = {
    val red: Color = Color.Red
    println(red)
  }
}

このような紋切型のコードを書いた経験のあるScalaプログラマーは多いでしょう。Scala 3では、同じことを次のように書くことができます。

enum Color {
  case Red, Green, Blue
}

def main(args: Array[String]): Unit = {
  val red: Color = Color.Red
  println(red)
}

記述が大幅に簡潔になっていることがわかります。

代数的データ型の例も挙げてみます。List のような代数的データ型 MList を実現するScala 2のコードです。

sealed abstract class MList[+A]

object MList {
  case class Cons[+A](val head: A, val tail: MList[A]) extends MList[A]
  case object Nil extends MList[Nothing]
}

object Usage {
  def main(args: Array[String]): Unit = {
    val xs = MList.Cons(1, MList.Cons(2, MList.Nil))
  }
}

これをScala 3では次のように書けます。

enum MList[+A] {
  case Cons(head: A, tail: MList[A])
  case Nil
}

def main(args: Array[String]): Unit = {
  val xs = MList.Cons(1, MList.Cons(2, MList.Nil))
}

紋切型の、 extends ... がなくなったおかげで記述が大幅にすっきりしています。Scala 2と同じ記述もコンパイルを 通るので安心してください。enumScala 2での同様なコードへのシンタックスシュガーとして実装されているためです。

enumは非常に多く使われるであろう機能で、これだけでも大幅に便利になることでしょう。

Implicitの見直し

implicitはScalaをもっとも特徴づける機能の一つであり、Scalaがパワフルである大きな要因でもある一方で、一つの機能の応用範囲があまりにも幅広かったためわかりにくかったり誤用されたりしたという歴史があります。Scala 3では、implicitの用途について詳細に分類し、それぞれ以下のような解決策を提供しました。

Type classes

type class (型クラス)は、Scalaにimplicit parameterという機能が導入されたそもそもの理由です。元々は型クラスがimplicit parameterの想定用途でした。一方で、型クラスが言語仕様に存在しているHaskellに比べると記述が冗長になる傾向がありました。

たとえば、現在のScalaではMonoid型クラスを以下のようにして定義する必要があります。

trait Monoid[A] {
  def plus(a: A, b: A): A
  def zero: A
}

implicit object IntMonoid extends Monoid[Int] {
  def plus(a: Int, b: Int): Int = a + b
  def zero: Int = 0
}


implicit object StringMonoid extends Monoid[String] {
  def plus(a: String, b: String): String = a + b
  def zero: String = ""
}

object Usage {
  def msum[A](xs: List[A])(implicit m: Monoid[A]): A = xs.foldLeft(m.zero)((x, y) => m.plus(x, y))

  def main(args: Array[String]): Unit = {
    val xs = List(1, 2, 3)
    println(msum(xs)) // 6

    val ys = List("A", "B", "C")
    println(msum(ys)) // "ABC"
  }
}

Monoidは、0に相当する値を返すzero メソッドと、2つの値を足すplus()メソッドの2つを持っています。これを使うことで、Monoidが定義されているあらゆる型に対して、Listの要素の合計値を返すmsumメソッドを定義できているのがポイントです。

ほぼ同じ定義をHaskellでは次のようにして書くことができます。

{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}

class MyMonoid a where
  zero :: a
  plus :: a -> a -> a

instance MyMonoid Integer where
  zero = 0
  x `plus` y = x + y

instance MyMonoid String where
  zero = ""
  x `plus` y = x ++ y

msum :: MyMonoid a => [a] -> a
msum xs = foldl plus zero xs

main = do
  let xs = [1, 2, 3] :: [Integer]
  putStrLn $ show $ msum xs
  let ys = ["A", "B", "C"] :: [String]
  putStrLn $ show $ msum ys

Scalaよりも型クラスの定義とインスタンスの宣言を簡潔に書けています。同じことをScala 3では次のように書くことができます。

trait Monoid[A] {
  def plus(a: A, b: A): A
  def zero: A
}

// MonoidのインスタンスをIntMonoidという名前で定義
given IntMonoid as Monoid[Int] {
  def plus(a: Int, b: Int): Int = a + b
  def zero: Int = 0
}

// Monoidの無名のインスタンス
given as Monoid[String] {
  def plus(a: String, b: String): String = a + b
  def zero: String = ""
}

// Monoidを使う
def msum[A](xs: List[A]) given (m: Monoid[A]): A = xs.foldLeft(m.zero)((x, y) => m.plus(x, y))

def main(args: Array[String]): Unit = {
  println(msum(List(1, 2, 3)))
  println(msum(List("A", "B", "C")))
}

目を引くのは given という新しいキーワードです。これは現在のScalaでいう、implicitなインスタンスを定義するためのキーワードです。 given IntMonoid as Monoid[Int] は、 Monoid[Int]インスタンスIntMonoid という名前で与えられると読むことができます。Scala 3では型クラスのインスタンスgiven で与えられるので、 implicit よりもわかりやすくなっています。

また、given as Monoid[String] のように、無名の型クラスのインスタンスを定義することもできます。型クラスのインスタンスを定義するときには、名前を必要としないことがほとんどなので、名前を考えなくて済むのは嬉しいところです。

型クラスを利用する側での、 given の後の引数名は省略することができます。その場合は次のように記述することになります。

def msum[A](xs: List[A]) given Monoid[A]: A = {
  val m = the[Monoid[A]]
  xs.foldLeft(m.zero)((x, y) => m.plus(x, y))
}

これは現在のScalaで以下のコードを書いたのと同等です。

def msum[A:Monoid](xs: List[A]): A = {
  val m = implicitly[Monoid[A]]
  xs.foldLeft(m.zero)((x, y) => m.plus(x, y))
}

implicitly という、「黒魔術的」に思われがちな名前の代わりに the と書くようになったことで、意図が伝わりやすくなっているのではないかと感じます。

# 2019年9月30日追記:

1週間前にリリースされたDotty 0.19.0-RC1では、givenの構文が一部変更されました。具体的には、given asが使えなくなり、以下のように書くことになりました。本記事で明記したバージョンではasが使えるので誤りではありませんが、記事公開前だったため追記しておきます。

// MonoidのインスタンスをIntMonoidという名前で定義
given IntMonoid: Monoid[Int] {
  def plus(a: Int, b: Int): Int = a + b
  def zero: Int = 0
}

// Monoidの無名のインスタンス
given: Monoid[String] {
  def plus(a: String, b: String): String = a + b
  def zero: String = ""
}

また、 the の代わりに summon を使うようになったため、msumのコードは以下のようになります。

def sum[A](xs: List[A]) given Monoid[A]: A = {
  val m = summon[Monoid[A]]
  xs.foldLeft(m.zero)((x, y) => m.plus(x, y))
}

なお、冒頭でことわったように、Scala 3の仕様が確定するまでは、まだこのような細かいレベルでの構文の変更が発生する可能性はあります。

Implicit conversions

implicit conversion (暗黙の型変換)は、Scalaのimplicitが提供する機能の中でも初期に濫用されたため、現在のScalaコミュニティではあまり使うべきではない機能とされています。また、初学者にとっての理解のハードルを上げる一因にもなっていました。Scala 3では、implicit conversionは、scala.Conversionクラスのインスタンスとして定義されます。以下は、IntBooleanに変換するimplicit conversionです。

given i2b as Conversion[Int, Boolean] = (value) => value != 0

implicit conversionの定義がgiven ... Conversion[A, B] という形になったので、どこで定義されているかが視覚的にわかりやすくなりました。とはいえ、現在のScalaと同じく、Scala 3でもimplicit conversionは多用すべきではありません。また、現在のScalaと同様に、利用するためには以下のimportが必要です。

scala.language.implicitConversions
Implicit function types

現在のScalaでは、implicit parameterを使って、ある文脈を表す値を渡し回す、という手法が多用されています。たとえば、よくある使い方として、データベースのトランザクションやセッションを表す値をimplicit parameterとして渡すという方法です。

以下は、Transaction型がデータベースのトランザクションを表すものとして、それを渡し回すコードの例です(Scala公式ページのブログ記事より引用)。

  def f1(x: Int)(implicit thisTransaction: Transaction): Int = {
    thisTransaction.println(s"first step: $x")
    f2(x + 1)
  }
  def f2(x: Int)(implicit thisTransaction: Transaction): Int = {
    thisTransaction.println(s"second step: $x")
    f3(x * x)
  }
  def f3(x: Int)(implicit thisTransaction: Transaction): Int = {
    thisTransaction.println(s"third step: $x")
    if (x % 2 != 0) thisTransaction.abort()
    x
  }
  def main(args: Array[String]) = {
    transaction {
      implicit thisTransaction =>
        val res = f1(args.length)
        println(if (thisTransaction.isAborted) "aborted" else s"result: $res")
    }
  }

このように、ある種の文脈情報を複数のメソッド間でimplicit parameterで共有する手法は非常に多用されていますが、一方で、コードが冗長であり、implicit parameterの利用目的が判然としないという問題がありました。

Scala 3では、implicit function typeという概念を導入して、この問題を解決しています。implicit function typeは、 implicit parameterを引数に取る特殊なfunction typeです。implicit function typeは以下のように表記することができます。

implicit T => R

implicit function typeは、直観的には「implicit parameterを取る関数」を抽象化したものとみることができます。implicit function typeを使うと、上のコードは以下のように書くことができます。

def thisTransaction: Transaction = implicitly[Transaction]
type Transactional[T] = implicit Transaction => T
def f1(x: Int): Transactional[Int] = {
  thisTransaction.println(s"first step: $x")
  f2(x + 1)
}
def f2(x: Int): Transactional[Int] = {
  thisTransaction.println(s"second step: $x")
  f3(x * x)
}
def f3(x: Int): Transactional[Int] = {
  thisTransaction.println(s"third step: $x")
  if (x % 2 != 0) thisTransaction.abort()
  x
}

implicit function typeは、通常の型なので、typeを使って型エイリアスを作ることができます。implicit function typeを導入する前のコードに比べて紋切り型のコードが減っています。また、型エイリアスを使うことによって、Transactional[Int]という共通の文脈を持っているということより明確になっています。

まとめ

Scala 3では、Scala 2では単一の概念であったimplicitを、用途別に異なる構文で書けるようにしています。implicitが何かよくわからない、という疑問にはよく遭遇するため、このようにして、implicitが用途に応じて違う見た目で書けるようになったのは良いことだと言えそうです。

Union types

現在のScalaでは、型Aと型Bのどちらかを取る型といったものをそのままでは記述することができません。たとえば、ログインのために、メールアドレス+パスワード、あるいは、電話番号+パスワードの2通りを引数に取って何らかの処理を行うメソッドを考えます。現在のScalaで素直に記述するとたとえば次のようになるでしょう。

sealed trait LoginInfo
case class EmailBasedLoginInfo(emailAddress: String, password: String) extends LoginInfo
case class PhoneNumberBasedLoginInfo(phoneNumber: String, password: String) extends LoginInfo
object Usage {
  def process(info: LoginInfo): Unit = info match {
    case EmailBasedLoginInfo(address, passsword) => ???
    case PhoneNumberBasedLoginInfo(phoneNumber, password) => ???
  }
}

このような、2つの型のどちらか一方を取る、という型を一般にunion typeといいます。Scala 3のunion typeを使うと、先ほどのコードは以下のように書くことができます。

case class EmailBasedLoginInfo(emailAddress: String, password: String)
case class PhoneNumberBasedLoginInfo(phoneNumber: String, password: String)
def process(info: EmailBasedLoginInfo | PhoneNumberBasedLoginInfo): Unit = info match {
  case info:EmailBasedLoginInfo => ???
  case info:PhoneNumberBasedLoginInfo => ???
}

union typeを使うよりも従来のアプローチの方がいい場合もあるので、常にこのように書かないといけないわけではありません。とはいえ、このように、2つの型のどちらかを受け取って処理をしたいという事は時々あるので、あると嬉しいことが多いというのが個人的な実感です。

その他の新機能

今回、特に従来のScalaユーザーにとって影響が大きいと思われる新機能を紹介しましたが、その他にも紹介していない機能が多数あります。たとえば、以下のような機能については今回紹介しませんでした。

  • Intersection types
  • Type lambdas
  • Dependent function types
  • Trait parameters

これらの機能の詳細は、Dottyのドキュメントを参照していただければと思います。

Scala 3で削除される機能

Scala 3では、Scala 2の機能のうち、混乱を招く構文のいくつかが削除されています。Scala 3は新機能の追加だけでなく、言語をよりシンプルにするためにいくつかの構文を削除していますが、Scala 2でこれらの機能を使っていた場合に若干の注意が必要となります。

XML literals

XML literalは、XMLをそのままScalaの値として記述できる構文です。

val doc = <foo>
  <bar>Hoge</bar>
</foo>

このとき、docにはパーズ済みのXMLが入るため、XMLを処理するようなプログラムでは便利な機能でした。一方で、Scalaが最初に開発されたときとは異なり、XMLの重要性が低下してきたこともあり、この機能の重要性は減っています。そこで、Scala 3では、XML literalを正式に削除することになりました。現在のScala 2.13でもそのままでは使えませんが、ライブラリの依存性を追加すれば使えます。

Auto-tupling

この機能は、そもそも使っていることを意識したことがないかもしれません。たとえば、現在のScalaでは、以下のような記述が許されます。

def foo(x: (Int, Int)): Int = x match {
  case (a, b) => a + b
}

foo((1, 2)) // (A)

foo(1, 2) // (B)

本来、タプルは(...)のように記述する必要がありますし、メソッド呼び出しの時の()もレシーバがない場合は省略することができません。本来なら、(A)のように書くべきです。しかし、現在のScalaでは、利便性のために(B)のように書くことも許しています。この機能をAuto-tuplingと呼びます。

Auto-tuplingは便利なこともありますが、型エラーが発生したときのメッセージがわかりにくくなるというデメリットの方が大きいものでした。これがなくなることによって、よりScalaが理解しやすくなると言えるでしょう。

2019/09/30追記:Auto-tuplingの削除はこのPRで議論されていますが、異論などがあって確定していないようです。

Automatic () insertion

この名前こそ聞き慣れないものの、この機能はみなさんが日頃使っているのではないかと思います。たとえば、以下のメソッド定義は、

def hello(): Unit = {
  println("hello")
}
hello()

と呼ぶこともできます。その一方で、

hello

()を省略して呼ぶこともできます。

これは便利な一面で、特に初学者にとって、Scalaの学習コストを上げる一因となっています。Scala 3では、()をつけたメソッド定義を()なしで呼び出すと、次のようなエラーメッセージがコンパイル時に出ます。

scala> def foo(): Int = 1
def foo(): IntCompile / compile 0s

scala> foo                                                                           
1 |foo
  |^^^
  |method foo must be called with () argument

メソッドfoo()をつけて呼び出さなければいけないと報告してくれているため、何が問題なのかもわかりやすくなっています。ただし、現状ではJavaのメソッドに関しては特別扱いしているようで、

System.currentTimeMillis

のような、()なしのメソッド呼び出しが許されています。

この機能の削除によって、Scalaがわかりやすくなった一方で、既存のコードでこれを利用している場合は注意が必要でしょう。

Multi-parameter infix operators

この機能もやはり聞き慣れないことが多いと思います。ただし、実際にはしばしば利用されている機能です。たとえば、以下のようなコードを見たことがある人は少なくないと思います。

val xs = scala.collection.mutable.Buffer.empty[Int]
xs += (1, 2, 3) // 1, 2, 3をxsに追加

このように、中置記法で複数引数のメソッド呼び出しを書くことができる記法をmulti-parameter infix operatorと呼びます。

ちなみに、これは、以下のように書くのと同じ意味です。

val xs = scala.collection.mutable.Buffer.empty[Int]
xs.+=(1, 2, 3) // 1, 2, 3をxsに追加

この機能はしばしば使われる一方で、わかりにくいエラーが出る一因にもなっていました。Scala 3ではこの機能が削除される予定です。ただし、これをエラーにするか非推奨にするかについては、議論が続いているようです。

Procedure syntaxes

procedure syntaxは、Unitを返すメソッド定義の糖衣構文です。Scalaのメソッド定義の内、Unit型を返すものについては、

def hello(): Unit = {
  println("hello")
}

と書く代わりに、次のように書くことができます。

def hello() {
  println("hello")
}

この機能で節約できるタイプ数はわずかな割に、混乱が大きかったため、Scala 3では正式に削除されています。また、マイグレーションのため、Scala 2.13では既に非推奨(deprecated)になっています。

Limit 22

Scalaでは、関数オブジェクトを以下のようにして定義できます。

val f: Int => Int = x => x

しかし、現状では、関数オブジェクトの引数の個数は22個までに制限されていました。たとえば、以下のコードはエラーになります。

val f: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) => Int = null
       error: function values may not have more than 22 parameters, but 30 given

また、タプルについても同様の制限があります。

 val t: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int) = null
       error: tuples may not have more than 22 elements, but 30 given

この制限は普段のプログラミングで問題になることは少ないですが、O/Rマッパーなどを利用する局面で、もっと多くの引数を持つ関数オブジェクトやタプルを持てないと不便なことがあります。Scala 3ではこの22個の制限が緩和されます。

Scala 3の仕様として何個までOKかについては決まっていないようですが、現在のところ、30個以上の引数を取る関数オブジェクトやタプルが作れます。

Scala 2とScala 3の互換性

Scalaを現在実用で利用されている方にとって気になることは、Scala 2のコードがScala 3のコードに対して後方互換性があるかということではないかと思います。

Scala 3で削除された機能がいくつかあるため、Scala 3のコードはScala 2に対して完全に後方互換であるとはいきません。これについては、Scala 2.13でprocedure syntaxがdeprecatedになったりなど、段階的なマイグレーションを意識してScala 2のメジャーバージョンアップが行われています。最新のScalaコンパイルできるコードにアップグレードすることでScala 3への移行がより容易になるのではないかと思います。また、Scala 3への移行に関してはscalafixによる自動書き換えが提供されるため、scalafixを利用することで移行はかなり楽にできるのではないかと思います。

標準ライブラリについていえば、Scala 3でもScala 2の標準ライブラリがそのまま使えるようになっているので、最新のScalaに追従できていれば、アップグレードのコストは大きくないでしょう。また、Scala 2で通ったコードがScala 3で型エラーや構文エラーになるケースは、試したところでは多くないように思います。

マクロのような「実験的な」(experimental)機能を利用しているライブラリにいくつかのプロジェクトが依存していますが、Scala 3のメタプログラミングの機能はScala 3のマクロと互換性がないため、そのことが移行にあたって問題になるかもしれません。

Scala 3を試す

Scala 3の仕様は現状でまだfixしていませんが、実装されている機能については、Dottyで試すことができます。

公式ページに書いてあるように、macOSの方は

brew install lampepfl/brew/dotty

で手軽にインストールできます。また、それ以外の環境の方でも、

sbt new lampepfl/dotty.g8

で、DottyをコンパイラにしたScalaプロジェクトが作れるので、sbtを使い慣れているみなさんなら簡単に試すことができます。また、ライブラリについても、Scala 2.12で実装されている標準ライブラリがそのまま使えるので、新機能の書き味だけを試すことも可能です。

興味を持った方は、是非、DottyでScala 3の新機能を試してみてください。

おわりに

この記事では、Scala 3の新機能や削除された機能について簡単に紹介しました。Scala 3は2020年中にはリリース予定となっています(これまでのリリーススケジュールを考えると、遅れる可能性が高いのではないかと私は考えていますが)。

たとえ予定通りにリリースされても、Scala 2のライブラリの追従状況などもあり、すぐにプロジェクトで使うとはいかないだろうと思いますが、Dottyを使って一足早くScala 3の機能や使い心地を堪能することができます。この記事が、Scala 3(Dotty)について調べたり試したりするきっかけになれば幸いです。

採用情報

そんなOpt Technologiesのエンジニアとお話ししてみたい方は、こちらより「カジュアル面談希望」とお声がけください!

Opt Technologiesでは新卒エンジニアも募集中です!ご応募はこちらから!

更新履歴