この記事は、 FOLIO Advent calendar 2020 の22日目の記事です。
これは何?
「Scala3の予習したいけど時間がない!」「まだM3だしガチでキャッチアップするのはちょっと…」という人のために、公式ドキュメントを斜め読みした自分が仕事で影響が大きそうなScala3の変更点をまとめたものです。
ほとんどのサンプルコードは、Dottyの公式ドキュメントから引用しております。
val,def,型エイリアスがトップレベルに書けるようになった
Scala2では、全てのval,def,型エイリアスは何かしらのclass,trait,objectなどの中に書かなければエラーになりましたが、Scala3では(まるでREPLのように)class外にdefやvalを定義することができるようになりました。package object
が要らなくなりますね。
package example
def f = println("Hello, World!")
インデントベースでコードが書ける
Scala3ではPythonのようにインデントで構文を書くことができます。これは少し前に話題になりましたね。
def greet =
println("Hello")
println("World!")
-noindent
フラグで一応無効もできるようです。
Intersection Types と Union Types
TypeScriptなど最近の言語ではおなじみとなったIntersection Types
と Union Types
がScalaでも使えるようになりました。
Intersection Types
&
オペレータを使い、A & B
と書くことでAでありBでもある型の値
を表現することができます。
以下は、Resettable
とGrowable[String]
両方を満たす型の値を受け取る関数f
を定義しているサンプルコードです。
trait Resettable:
def reset(): Unit
trait Growable[T]:
def add(t: T): Unit
def f(x: Resettable & Growable[String]) =
x.reset()
x.add("first")
Union Types
こちらは、|
オペレータを使い、A | B
と書くことでAとBに属する全ての値を持つ型
を表現することができます。
case class UserName(name: String)
case class Password(hash: Hash)
def help(id: UserName | Password) =
val user = id match
case UserName(name) => lookupName(name)
case Password(hash) => lookupPassword(hash)
...
enumが追加された
Scala2でもいくつか列挙型の定義方法がありましたが、新たにenum
が追加されました。
enum Color:
case Red, Green, Blue
上記コードは、Color.Red
, Color.Green
, Color.Blue
の3つの値を持つColor
という名前のsealed class
を定義します。値はコンパニオンオブジェクトの中に定義されます。
enum
はsealed trait
と同じようにメソッドを生やすことができます。
enum Color:
case Red, Green, Blue
def isRed: Boolean = this == Color.Red
パラメータ付きenum
以下のように、パラメータ付きのenum
を定義することもできます。sealed abstract class
と sealed trait
を使い分ける必要がなくなりますね。
enum Color(val rgb: Int):
case Red extends Color(0xFF0000)
case Green extends Color(0x00FF00)
case Blue extends Color(0x0000FF)
enumの値はcaseの並び順で0から採番されており、ordinal
で整数値に変換したりfromOrdinal
でenumの値に戻すことができます。
scala> Color.Red.ordinal
val res2: Int = 0
scala> Color.fromOrdinal(res2)
val res3: Color = Red
それ以外にも、文字列から値を作るvalueOf
や全ての値を得ることができるvalues
などのAPIも用意されており、まるでボイラープレートの少ないenumeratumのような機能となっています。
scala> Color.valueOf("Blue")
val res0: Color = Blue
scala> Color.values
val res1: Array[Color] = Array(Red, Green, Blue)
代数的データ型(ADT)としてのenum
enum
はADTを定義するのにも利用できます。以下は、enum
でOptionを定義するコードです。
enum Option[+T]:
case Some(x: T)
case None
パラメータ付きのcase Some(x: T)
は、Option[T]
を継承するcase class
を簡潔に定義するための記法です。
case None
は、Tが共変のため、Option[Nothing]
に推論されています。
extends
句は以下のように明示的に書くこともできます。
enum Option[+T]:
case Some(x: T) extends Option[T]
case None extends Option[Nothing]
traitがパラメータを持てるようになった
Scala2ではパラメータのありなしでabstract class
とtrait
使い分けたりしてましたが、その必要がなくなりそうです。
trait Greeting(val name: String):
def msg = s"How are you, $name"
class C extends Greeting("Bob"):
println(msg)
Implicitが色々変わった
今までややこしくて「怖い」などと言われていたImplicitが整理されました。
Implicit Parametersはgivenとusingになった
implicit
パラメータの定義はgiven、使う側はusingと書くようになりました。
以下は、Ordering
ライクな型クラスOrd
とOrd[Int]
,Ord[List[T]]
のgivenを定義するサンプルコードです。
trait Ord[T]:
def compare(x: T, y: T): Int
extension (x: T) def < (y: T) = compare(x, y) < 0
extension (x: T) def > (y: T) = compare(x, y) > 0
given intOrd: Ord[Int] with
def compare(x: Int, y: Int) =
if x < y then -1 else if x > y then +1 else 0
given listOrd[T](using ord: Ord[T]): Ord[List[T]] with
def compare(xs: List[T], ys: List[T]): Int = (xs, ys) match
case (Nil, Nil) => 0
case (Nil, _) => -1
case (_, Nil) => +1
case (x :: xs1, y :: ys1) =>
val fst = ord.compare(x, y)
if fst != 0 then fst else compare(xs1, ys1)
givenは無名でも定義することができます。
given Ord[Int] with
...
given [T](using Ord[T]): Ord[List[T]] with
...
Alias givens
エイリアスでgivenインスタンスを定義することもできます。
この例では、最初のglobal
へのアクセス時のみForkJoinPool()
が評価され、以降のアクセスはそれを返却します。
given global: ExecutionContext = ForkJoinPool()
Implicit Conversionは、Conversionという型クラスになった
Scala3ではConversionのgivenインスタンスを定義することで、Implicit Conversionが使用できます。
以下のサンプルコードでは、printPrice
の中でInt
からPrice
への暗黙的型変換を行っています。
case class Price(value: Int):
def show: String = s"${value.toString}円"
given Conversion[Int, Price] = Price(_)
def printPrice(intPrice: Int)(using Conversion[Int, Price]): Unit = println((intPrice: Price).show)
==がまともになった
==
と!=
がCanEqual[A, B]
のgivenインスタンスを使って比較するようになりました。間違って異なる型の値同士を比較していたり(警告出ますが)、canEqual
が実装されてなくてオブジェクトIDで比較していたなんてことが減るので安全性が向上しますね。
scala> 1 == "foo"
1 |1 == "foo"
|^^^^^^^^^^
|Values of types Int and String cannot be compared with == or !=
Explicit Nulls
こちらは-Yexplicit-nulls
を付与すると有効化されるオプトイン機能ですが、Scalaの型システムに変更を加え、AnyRef
を継承する値をnon-nullableにできます。堅く作りたいうちの会社のシステムなんかには合いそうな気がします。
val x: String = null // error: found `Null`, but required `String`
Nullableな値を表現したい場合は、Union Typesを使います。
val x: String | Null = null // ok
以下は有効化後の型ヒエラルキーの図です。NullがAnyRefから独立した型になっているのが判ります。
2. @main
でエントリーポイントが定義できるようになった
Scala3ではdefに@main
アノテーションを付与すれば、それがエントリーポイントになります。
@main def greet = println(s"Hello, World!")
> run
[info] running greet
Hello, World!
@main
で定義したエントリポイントは、引数でコマンドライン引数を受け取ることができます。
@main def greet(name: String) = println(s"Hello $name!")
> run John
[info] running greet John
Hello John!
[success] Total time: 0 s, completed 2020/12/28 1:00:19
> run
[info] running greet
Illegal command line: more arguments expected
[success] Total time: 1 s, completed 2020/12/28 1:28:03
Tupleの展開がちょっと便利になった
Tupleのそれぞれのフィールドの値をmap
などの高階関数の中で変数に展開したい場合にcaseを書かなくてもよくなりました。
scala> val xs: List[(Int, Int)] = (1, 2) :: (3, 4) :: Nil
|
| xs.map {
| (x, y) => x + y
| }
val xs: List[(Int, Int)] = List((1,2), (3,4))
val res6: List[Int] = List(3, 7)
Scala3で削除される機能たち (一部抜粋)
XMLリテラル
まだ使えますが、近いうちに xml"..."
補間子に置き換えられるとのこと。
Scala2マクロ
Scala2の実験的マクロシステムは削除されました。
代わりとして、Scala3で新たに実装された inline
と '{ ... }
/${ ... }
によるコード生成が利用できるようです(よくわかっていない)。
Symbolリテラル
既に削除されており、使えなくなっています。
あまりプロダクションのコードで使ってるのを見たことがありませんが、ScalaTestの記法などに影響がありそうです。
scala> 'a
1 |'a
|^
|symbol literal 'a is no longer supported,
|use a string literal "a" or an application Symbol("a") instead,
|or enclose in braces '{a} if you want a quoted expression.
22制限
Tupleのフィールド数やFunctionのパラメータ数は最大22までに制限されてましたが、Scala3では任意の数をもたせることができるようになります。
22を超えるとFunctionはscala.FunctionXXL
、Tupleはscala.TupleXXL
になるようです。
scala> val tuple23 = (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23)
val tuple23: (Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int, Int,
Int
, Int, Int, Int, Int, Int, Int, Int, Int, Int) = (1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23)
scala> tuple23.getClass.getName
val res5: String = scala.runtime.TupleXXL
Auto-Application
()
付きのdefは()
なしでは呼べなくなりました。
scala> def hello() = println("Hello, World!")
def hello(): Unit
scala> hello
1 |hello
|^^^^^
|method hello must be called with () argument
private[this] と protected[this]
非推奨になり、段階的に削除予定。
package object
まだ使用できますが、いずれ非推奨になり、その後削除されるようです。
トップレベルになんでも書けるようになったので要らないですね。
最後に
ここに書いた変更点は全部ではありません。しっかり学びたいと思った方は公式ドキュメントやサンプルコードを読まれるのをおすすめします!