続・Dottyによる変更点
以前以下のようなDottyに関する記事を書きました.
それから暫く経ち, 多くの更新が入ったため新しくまとめました. 公式ドキュメントだけでなくあちこちにちらばった情報を集めています. 過去記事と同じくあくまで真新しい機能のみに注目し, 理論的な部分や実装については深掘りしません. 概要のみなので, 個々の詳細は各見出しのリンク先を御覧ください.
Dottyとは
Dependent Object Types (DOT) に基づいた新しいScalaコンパイラです. Scala3系に相当します. DOT計算理論の詳細は省きます (まとめるほど理解できていないとも言う…). 現行のコンパイラと比べ, コンパイラサイズの減少・コンパイル速度向上・様々な機能追加の他, コンパイラ自体の開発安定性も増すパワフルなアップグレードが期待できます. 現行のScalaとの互換性は切れていますが, 勿論重要な機能がなくなるといったことはなく移行ツールも用意されています (詳細後述). また, Scala2.11系でビルドされたライブラリであればそのまま使えます (2.12系は現時点では無理なようです).
Dottyの変更点
以下では現行のScalaをScala2と表記します. なお, 一部Dottyの新機能として公式で紹介されているものの, Scala2でも利用可能な機能があります. それについては見出しに [Scala2]
を付けています.
なお, なんとなく関係してそうな機能順に並べていますが, 変更点が多岐に渡るため結構バラバラです.
Union Types
Union typesは A | B
という形で A
と B
から成る合併型を表します.
class A(x: Int) class B(x: Int) def foo(x: A | B): Int = x.x foo(new A(10)) //=> ok foo(new B(20)) //=> ok
Scala2では Any
とするしかないようなケースも, Dottyでは合併型として扱うことができます. ただし手元で試した限りは, 現状明示的に型アノテーションを付けないと Any
になってしまうようです.
val bar: Int | String = if (cond) 1 else "string" val baz: List[Int | String] = List(1, "string") val _bar = if (cond) 1 else "string" val _baz = List(1, "string") // _bar: Any // _baz: List[Any]
合併型はパターンマッチ時も特別扱いされ, 網羅性に漏れがあると警告が出ます.
Intersection Types
Intersection typesは A & B
という形で A
と B
から成る交差型を表します. Scala2でも with
を使うことで交差型を表現できますが, Dottyでは順序がなくなり, 正確な交差を表現するようになります.
trait A trait B // Scala2の交差型 // A with B != B with A // Dottyの交差型 // A & B == B & A // 可換になった trait Resettable { def reset(): this.type } trait Growable[T] { def add(x: T): this.type } def f(x: Resettable & Growable[String]) = { x.reset() x.add("first") }
Dependent Function Types
Dependent MethodはScala2でも利用できましたが, Function Typeにも拡張されます. 以下の例だと, extractKey
の返り値型 (e.Key
) がDependent Type, extractor
の型 ((e: Entry) => e.Key
) がDependent Function Typeです.
trait Entry { type Key; val key: Key } def extractKey(e: Entry): e.Key = e.key // a dependent method val extractor: (e: Entry) => e.Key = extractKey // a dependent function value
Improved Type Inference
型推論器「われわれはかしこいので」
一番目立つ改善点としては, (型推論のための) カリー化が不要になります.
def foo[A](a: A, f: A => A): A = f(a) // Scala2ではこのように型情報を明示するか foo(1, (x: Int) => x * 2) // カリー化する必要があった def bar[A](a: A)(f: A => A): A = f(a) bar(1)(x => x * 2) // 一方Dottyはどちらも不要でシンプル! foo(1, x => x * 2)
Scala2では前の引数に依存する型情報 (f
のための A
) を利用するためには型情報を明示するかカリー化する必要がありましたが, Dottyではこのような場合でもカリー化することなく型推論が可能になります.
この例以外にも細かい改善が含まれます.
Literal-based Singleton Types
値が型のような振る舞いをできるようになります.
object Literals { val fortyTwo: 42 = 42 val `2`: 2 = 2 val fortyFour: 44 = fortyTwo + `2` val text: "text" = "text" def id[T](a: T) = a val two: 2 = id(`2`) }
値を型引数として渡してやったり.
forAll { x: Ranged[Int, 1, 100] => val n: Int = x.value // guaranteed to be 1 to 100 }
Enum
Scala2でEnumを実現するには sealed class + case object
か Enumeration
クラスを利用する方法がありましたが, Enum用の独立した構文 (糖衣構文) が導入されます.
enum Color {
case Red, Green, Blue
}
代数的データ型の表現も行うため, ユーザ定義のパラメータ等を持たせることもできます.
enum Color(val rgb: Int) { case Red extends Color(0xFF0000) case Green extends Color(0x00FF00) case Blue extends Color(0x0000FF) }
その他, Enumの値や名前から要素を取得するといったユーティリティが提供されます.
Trait Parameters
トレイトにパラメータが渡せるようになります. 内部的にはJava8で拡張された interface
と同じ扱いになるようです.
trait A(x: String) { println(x) } class B extends A("Hello") new B //=> Hello
代わりに以下のような事前定義は廃止されます.
// Dottyではコンパイルエラー class B extends { val x: String = "Hello" } with A new B //=> Hello
Dropped Class Shadowing
元々Scalaではクラスのoverrideは許可されていませんが, Scala2ではあたかもoverrideしているように見える記述 (shadowing) が可能でした. Dottyでは継承時に基底クラス内のクラスと同じ名前のクラスを宣言できなくなります.
class Base { class Ops { ... } } class Sub extends Base { class Ops { ... } // not allowed }
@static
Methods and Fields in Scala Objects
Scala2ではあくまでシングルトンしか扱えませんでしたが, アノテーションを使ってJavaの static
に相当する静的な定義ができるようになります.
object Foo { @static val x = 5 @static def bar(y: Int): Int = x + y }
[Scala2] Option-less Pattern Matching (name-based pattern matcher)
以下の2つのメソッドを実装するだけでextractor (unapply
) としてパターンマッチが可能になります. T
がプリミティブ型であれば, Option
に包む際のboxingがなくなるためパフォーマンス面の寄与もありそうです.
def isEmpty: Boolean def get: T
final class OptInt(val x: Int) extends AnyVal { def get: Int = x def isEmpty = x == Int.MinValue // or whatever is appropriate } // This boxes TWICE: Int => Integer => Some(Integer) def unapply(x: Int): Option[Int] // This boxes NONCE def unapply(x: Int): OptInt
Vararg Patterns
パターンマッチ中の可変長引数パターンの記法が変わります. Scala2では特別扱いされる _*
を @
による変数束縛と組み合わせる必要がありました. Dottyではメソッドの可変長引数と同じように _*
が型として扱われるようになります.
xs match { // case List(1, 2, xs @ _*) // Scala2 // case List(1, 2, _*) => // Scala2 case List(1, 2, xs: _*) => println(xs) // Dotty case List(1, _ : _*) => // Dotty (wildcard) }
Repeated By-name Parameters
=> T*
が Function0[Seq[T]]
を意味するようになります. Scala2ではなぜか可変長引数を名前渡しできませんでしたが, Dottyではそれができるようになります.
def foo(xs: => Int*): Option[Int] = xs.headOption // Scala2ではコンパイルエラー
Function with Very Many Parameters
引数22個制限がなくなります.
Multiversal Equality
==
や !=
が型安全になります.
Scala2では以下の様なことが起こりえます.
scala> 1 == "1" <console>:8: warning: comparing values of types Int and String using `==' will always yield false 1 == "1" res1: Boolean = false
Int
と String
を比較しているので常に false
になりますが, そもそもこのような比較は意図しない状況で発生していると思います. 上の例ではうまいこと警告が出たものの, 以下のように出ないことも多々あります.
scala> "1" == 1 res2: Boolean = false
ここに Eq
型クラスを導入することで意図しない比較をコンパイル時に弾けるようになります. Eq
が実装されていない場合は eqAny
にフォールバックして比較が行われるようです.
Dropped Weak Conformance
Weak Conformanceは以下のような複数の型を扱うことにありました. 複数の型うち, 最小上限? (日本語表現が分からん) にあたる型をチェックします. (もしないと, List[Double]
ではなく List[AnyVal]
として解釈されます).
List(1.0, math.sqrt(3.0), 0, -3.3) // List[Double] // Double, Double, Int, Double
しかしこの変換規則が明確ではないため, 代わりにプリミティブな数値型は以下のような順序から決定されるように変更されます. なお, メソッドの返り値等定数でない数値を変換する必要がある場合や, 変換時に精度が保てない場合は AnyVal
に落ちるようです.
Double / \ Long Float \ / Int / \ Short Char | Byte
Automatic Tupling of Function Parameters (Function Arity Adaption)
引数マッピングの効率が良くなります
val pairs = Seq((1, 2), (3, 4)) // Scala2ではこうする必要があったが pairs.map { case (x, y) => x + y } // Dottyでは簡潔に書けるようになります pairs.map((x, y) => x + y)
Type Lambdas
匿名関数のラムダ式のように型レベルでもラムダ記法が使えるようになりました.
type IntMap[X] = Map[Int, X] type IntMap = [X] => Map[Int, X] // type lambda
Scala2から存在したType Projection (#
) を使うType Lambdaも使えるようですが, 可読性から新しいType Lambdaを使ったほうが良さそうです.
trait Functor[F[_]] type F = Functor[({ type IntMap[X] = Map[Int, X] })#IntMap] // Both Scala2 and Dotty: ok type F = Functor[[X] => Map[Int, X]] // using new type lambda, Dotty only // type F = Functor[Map[Int, _]] // これは相変わらずできない
Named Type Parameters
型引数に名前を付けることができるようになり, 併せて名前付き型引数の部分適用が可能になります
trait Map[type Key, type Value] type IntMap = Map[Key = Int]
Implicit Function Type
簡単に言うと, コンテキスト等をimplicit parameterで引き回す際に生まれるボイラープレートを省略できるようになります.
type Viewed[T] = implicit Viewer => T def f(x: Int): Viewed[Double] = ??? // こう書くだけで // 最適化を経てカリー化されたimplicit parameterを使った形に展開される // def f(x: Int)(implicit v: Viewer): Double = ???
Changes in Implicit Resolution
implicit
な値に明示的なシグネチャが必要になるなど細かい点で幾つか変更があります. 詳細は見出しのリンク先を参照してください.
Contravariant Implicit
反変implicitが利用可能になります
trait A[-T] case class B[T]() extends A[T] class X class Y extends X implicit val x = B[X] implicit val y = B[Y] implicitly[A[X]] implicitly[A[Y]] // Scala2だとこれはコンパイルエラー
あまり例に自信がないけど多分こんな感じだと思います.
Implicit By-name Parameters
implicitな引数で名前渡しを利用できるようになります. 以下の例だと, Some(33)
の場合はevidenceの intCodec
が呼ばれますが, None
の場合は名前渡しになっているevidenceの評価は行われません.
trait Codec[T] { def write(x: T): Unit } implicit def intCodec: Codec[Int] = { println("intCodec was called") ??? } implicit def optionCodec[T] (implicit ev: => Codec[T]): Codec[Option[T]] = new { def write(xo: Option[T]) = xo match { case Some(x) => ev.write(x) case None => } } val s = implicitly[Codec[Option[Int]]] s.write(Some(33)) // print "intCodec was called" s.write(None) // none
Restrictions to Implicit Conversions
Scala2では Function1
かその子クラスであれば暗黙的な型変換の候補になっていましたが, Dottyでは ImplicitConverter
という Function1
の子クラスを明示したもののみが候補に制限されます.
implicit val m: Map[Int, String] = Map(1 -> "abc") // val x: String = 1 // Scala2: xにはimplicit conversionが行われた"abc"が代入される // Dotty: implicit conversionは行われずcompile error
Erased Terms
コンパイル時のevidenceとしてのみ利用される引数やメソッド等用の新しい修飾子 erased
が導入されます. implicit
と組み合わせることもできます. コンパイル時に使われた後はeraseされ, 実行時にはなくなります.
inline
modifier
インライン展開のヒント (強制?) になる新しい修飾子です. メソッドに対して付与すると単純にインライン展開されます. 変数に対して付与すると final
のようなconstantであるという意味を持ちます.
Scala2から @inline
というアノテーションとして存在しましたが, 基本的にはキーワードに置き換えられます (アノテーションは継続してサポートされます). また, Macrosにあったinlineも置き換えるようです.
inline val x = 10 inline def foo(str: String): Int = ... @`inline` val y = 20 // アノテーションも利用できるが, inlineがキーワードになるためバッククォートが必要になる
Dropped Auto-Application
Scala2では引数が空で宣言されたメソッドを, ()
を省略して呼び出すことができました. Dottyではこれができなくなり, 必ず ()
を付けなければなりません.
def next(): T = ... val t = next // Scala2: ok val t = next() // Scala2: ok // val t = next // Dotty: error val t = next() // Dotty: ok
※ Javaで定義されたメソッドは例外的にAuto-Applicationが行われます. 今まで通り, toString
等に括弧は必要ありません.
※ Auto-Applicationは引数リストを省略して宣言されたメソッドとは関係ありません. 今まで通り, 引数リストを省略して宣言されたメソッドは括弧なしで呼び出すことになります (括弧を付けて呼び出すとことはできません). また, 引数がないメソッドに括弧を付けるか付けないかの使い分けは, プロパティのような副作用がないものは括弧なし, 副作用があるものは括弧ありとすることが推奨されていますが, コンパイラによるチェック等はなくユーザに託されています (つらい).
def getOne: Int = 1 val one = getOne // Scala2, Dotty: ok // val one = getOne() // Scala2, Dotty: error
Automatic Eta Expansion
Scala2ではeta expansion (メソッド→関数(FunctionN) への変換) にアンダースコア (_
) が必要だったところが不要になります (今までは先述のAuto-Applicationと干渉して実現できてなかったのかな?). イメージとしてはJavaScriptとかが似ていると思う.
def m(x: Boolean, y: String)(z: Int): List[Int] val f1 = m val f2 = m(true, "abc") // f1: (Boolean, String) => Int => List[Int] // f2: Int => List[Int] // Scala2ではアンダースコアが必要だった // val f1 = m _ // val f2 = m(true, "abc") _
Dropped Procedure Syntax
返り値の Unit
と =
を省略する記法が廃止されます.
def foo() { ??? } // compile error
↓
def foo(): Unit = { ??? } // ok
Improved Lazy Vals Initialization
lazy
フィールドの初期化メカニズムを変更することで, 初期化の際の潜在的なデッドロックを防げるようになります.
以下の例だと, A
(A.a0
) と B
(B.b
) をそれぞれ別スレッドから初期化しようとするとデッドロックが起きる可能性がありますが, Dottyではこれが防がれます. 他にも特定パターンで発生するStackOverflowも防ぐようです. スレッドセーフにする場合は @volatile
が必須になります.
object A { @volatile lazy val a0 = B.b lazy val a1 = 17 } object B { @volatile lazy val b = A.a1 }
Dropped Existential types
forSome
ではなくワイルドカードで記述するように変更されます.
val x: Array[T] forSome { type T <: Foo } = ???
↓
val x: Array[_ <: Foo] = ???
Non-boxed Arrays of Value Classes
Value classの配列がboxing/unboxingしなくなります.
Dropped XML Literals
XML専用の構文がありましたが廃止されます. 代わりにString interpolationを使うことになります.
val data = xml"""some xml as string here""" // string interpolation
Meta programming
Macroが廃止され, scala.meta
or??? scala.macros
(廃止予定のMacroとは別物) ベースになる???
TODO
Programmatic Structural Types
TODO
ツールの性能向上
コンパイラの性能向上
- DOT計算ベースに刷新
- Encoding type parameters
高階型を表現するために型引数情報がメンバとして扱われるようになる. Named type parametersはこれをうまいこと利用してるみたい
trait List[T]
→trait List { self => type T }
- Encoding type parameters
- Phase fusionによるコンパイル高速化
- インクリメンタルコンパイルが賢く
- エラーメッセージが賢く
- TASTY
新しい中間ファイル. これを元にJVM/JS/Native向けのコードが吐かれる. マルチプラットフォーム化が容易になると思われる - Local optimisations
「final
なフィールドにリフレクションでアクセスしない」という条件で利用可能な最適化オプション.-optimise
というフラグで有効になる. - Dotty Linkerによる最適化
コンパイラ以外の便利ツール
Dottyとは別軸のものも含まれますが, 機能追加がされています.
- REPLにシンタックスハイライトが導入
- DottyDoc
- Language Server Protocol 対応
このプロトコルをサポートしている開発環境であればデバッガ等の恩恵を受けられるように (sbtのLSPとは別に開発されている模様)
まだ上記全ての機能が実装されたわけではなく, 上記以外に検討中の機能もあります (shapeless系, Non-nullable type, etc.). また, 一部機能はScala2にも実装が行われてい(ます|く予定です).
Dottyの使い方
まだプレビュー版ですがsbtプラグインとして提供されています. Java8が必須です. README通りにやればちょいちょいです.
別の方法として, Webベース実行環境のScastieで手軽にDottyを試すこともできます.
Scala2との相互運用
Scala2.11系でビルドされたライブラリを使うことができます. 例えばdependencyをこんな感じにすればok.
libraryDependencies += "org.json4s" % "json4s-native_2.11" % "3.5.1"
積極的に人柱になってバグ報告をすると喜ばれると思います.
Scala2からの移行
Scalafixというツールが公式に提供されています. こっちは試してないですが「2to3…ウッ頭が…」ってならないことを信じてます.
まとめ
いいこと尽くめなので早くDottyメインの世界になって欲しいところです. が, まだ実装が終わっていなかったり, 互換性が切れるのでどう普及していくかが気になるところです. ただ, ふんわりとしたロードマップが発表されたり (page 65 in What to leave implicit by Martin Odersky), バージョンも0.7まで上がってきたことを踏まえるとそう遠くない未来かもしれません.
参考文献
- Dotty公式ドキュメント
- Exploring the future of Scala by Dmitry Petrashko
- The state of Dotty by Guillaume Martres
- Dotty and the new Scala developer experience by Felix Mulder
- Dotty Linker: Making your Scala application smaller and faster by Dmitry Petrashko
- Implementing higher-kinded types in Dotty by Martin Odersky
- Scala the road ahead by Martin Odersky
- What to leave implicit by Martin Odersky