30
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

忙しい人のためのScala3予習メモ

Last updated at Posted at 2020-12-30

この記事は、 FOLIO Advent calendar 2020 の22日目の記事です。

これは何?

「Scala3の予習したいけど時間がない!」「まだM3だしガチでキャッチアップするのはちょっと…」という人のために、公式ドキュメントを斜め読みした自分が仕事で影響が大きそうなScala3の変更点をまとめたものです。

ほとんどのサンプルコードは、Dottyの公式ドキュメントから引用しております。

val,def,型エイリアスがトップレベルに書けるようになった

Scala2では、全てのval,def,型エイリアスは何かしらのclass,trait,objectなどの中に書かなければエラーになりましたが、Scala3では(まるでREPLのように)class外にdefやvalを定義することができるようになりました。package objectが要らなくなりますね。

example/foo.scala
package example

def f = println("Hello, World!")

インデントベースでコードが書ける

Scala3ではPythonのようにインデントで構文を書くことができます。これは少し前に話題になりましたね。

greet.scala
def greet =
  println("Hello")
  println("World!")

-noindentフラグで一応無効もできるようです。

Intersection Types と Union Types

TypeScriptなど最近の言語ではおなじみとなったIntersection TypesUnion TypesがScalaでも使えるようになりました。

Intersection Types

&オペレータを使い、A & Bと書くことでAでありBでもある型の値を表現することができます。
以下は、ResettableGrowable[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を定義します。値はコンパニオンオブジェクトの中に定義されます。

enumsealed traitと同じようにメソッドを生やすことができます。

enum Color:
   case Red, Green, Blue
   def isRed: Boolean = this == Color.Red

パラメータ付きenum

以下のように、パラメータ付きのenumを定義することもできます。sealed abstract classsealed 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 classtrait使い分けたりしてましたが、その必要がなくなりそうです。

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ライクな型クラスOrdOrd[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から独立した型になっているのが判ります。

explicit-nulls-type-hierarchy.png

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

まだ使用できますが、いずれ非推奨になり、その後削除されるようです。
トップレベルになんでも書けるようになったので要らないですね。

最後に

ここに書いた変更点は全部ではありません。しっかり学びたいと思った方は公式ドキュメントサンプルコードを読まれるのをおすすめします!

30
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
22

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?