読者です 読者をやめる 読者になる 読者になる

水底

ScalaとかC#とかネットワークとか

Dottyによる変更点と使い方

Dottyで何ができるようになるのかとその使い方を簡単にまとめたいと思います. 理論的な部分は深掘りしません.

f:id:amaya382:20170429233234p:plain

以下ちょっと長め. 調べきれてない点も多々あるので, 抜けや間違いがあったら教えてください.

そもそもDottyとは

Dependent Object Types (DOT) に基づいた新しいScalaコンパイラです. たまにScala3とか呼ばれたりもしています. DOT計算理論の詳細は省きます (まとめるほど理解できていないとも言う…). 現行のコンパイラと比べ, コンパイラサイズの減少・コンパイル速度向上・様々な機能追加の他, コンパイラ自体の開発安定性も増すパワフルなアップグレードが期待できます. 現行のScalaとの互換性は切れていますが, 勿論重要な機能がなくなるといったことはなく移行ツールも用意されています (詳細後述).

Dotty github.com

Dottyの新機能

以下では現行のScalaをScala2と表記します.

Union/Intersection Types

Union typesは合併型, Intersection typesは交差型を表しています. Scala2でも with を使うことで交差型を表現できますが, 順序がなくなり, より正確な交差を表現するようになります.

trait A { type T = Int }
trait B { type T = Double }

// Scala2の交差型
(A with B) # T //=> Int
(B with A) # T //=> Double

// Dottyの交差型
(A & B) # T //=> Int & Double
// (A & B) と (B & A) は等価
// Scala2と比べて順序がなくなった


// Dottyの合併型
class C(x: Int)
class D(x: Int)
def foo(x: C | D): Int = x.x
foo(new C(0)) //=> ok
foo(new D(0)) //=> ok

val bar = if (cond) 1 else "string"
// Scala2 => bar: Any
// Dotty  => bar: Int | String

// このような合併型はScala2では実現できなかった

合併型はパターンマッチ時も特別扱いされ, 漏れがあると警告が出ます.

Improved Type Inference

型推論器「われわれはかしこいので」

一番目立つ改善点としては, (型推論のための) カリー化が不要になります. っょぃ (小並感).

def foo[A, A](a: A, f: A => A): A = f(a)

// Scala2ではこのように型情報を明示するか
foo(1, (x: Int) => x * 2)

// カリー化する必要があった
def bar[A, 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
}

Trait parameters

trait にパラメータが渡せるようになります. 内部的には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

@static methods and fields in Scala objects

Scala2ではあくまでシングルトンしか扱えませんでしたが, Javastatic に相当する静的な定義ができるようになります.

object Foo {
  @static val x = 5
  @static def bar(y: Int): Int = x + y
}

Improved Lazy Vals Initialization

lazy フィールドの初期化メカニズムを変更することで, 初期化の際の潜在的デッドロックを防げるようになります.

以下の例だと, A (A.a0) と B (B.b) をそれぞれ別スレッドから初期化しようとするとデッドロックが起きる可能性がありますが, Dottyではこれが防がれます. 他にも特定パターンで発生するStackOverflowも防ぐようです.

object A {
  lazy val a0 = B.b
  lazy val a1 = 17
}
object B {
  lazy val b = A.a1
}

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

Repeated By Name Parameters

=> T*Function0[Seq[T]] を意味するようになります. Scala2ではなぜか可変長引数を名前渡しできませんでしたが, それができるようになります.

def foo(xs: => Int*): Option[Int] = xs.headOption
// Scala2ではコンパイルエラー

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

IntString を比較しているので常に false になりますが, そもそもこのような比較は意図しない状況で発生していると思います. 上の例ではうまいこと警告が出たものの, 以下のように出ないことも多々あります.

scala> "1" == 1
res2: Boolean = false

ここに Eq 型クラスを導入することで意図しない比較をコンパイル時に弾けるようになります. Eq が実装されていない場合は eqAny にフォールバックして比較が行われるようです.

Non-boxed arrays of value classes

Value classの配列がboxing/unboxingしなくなります.

ツールの性能向上

コンパイラの性能向上

  • DOT計算ベースに刷新
  • Phase fusionによる高速化
  • インクリメンタルコンパイルが賢く
  • エラーメッセージが賢く
  • TASTY
    新しい中間ファイル. これを元にJVM/JS/Native向けのコードが吐かれる. マルチプラットフォーム化が容易になったと思われる
  • Dotty Linkerによる最適化
    • Dead Code Elimination
    • Automatic specialization
      @specialize による手動最適化が不要に
    • Convert classes to value classes
      条件を満たしていれば自動的にValue classが利用される
    • Eliminate virtual dispatch
      インライン化して呼び出しの最適化
    • 諸所の最適化でproguardを使った場合よりもバイトコードのサイズがコンパクトになる

コンパイラ以外の便利ツール

その他の変更点

Macro to scala.meta

従来のマクロは廃止され, scala.meta に統一されます.

Procedure syntax

返り値の Unit が必須になります.

def foo() { ??? }

def foo(): Unit = { ??? }

Function with very many parameters

引数22個制限がなくなります.

存在型 (forSome)

forSome ではなくワイルドカードで記述するように変更されます.

val x: Array[T] forSome { type T <: Foo } = ???

val x: Array[_ <: Foo] = ???

まだ上記全ての機能が実装されたわけではありません. また, 上記以外にも検討中の機能があります (Java-like Enum, shapeless系, Non-nullable type, etc.).

Dottyの使い方

まだプレビュー版ですがsbtプラグインとして提供されています. Java8が必須です. README通りにやればちょいちょいです.

github.com

積極的に人柱になってバグ報告をすると喜ばれると思います.

Scala2からの移行

Scalafixというツールが提供されています. こっちは試してないですが「2to3…ウッ頭が…」ってならないことを信じてます.

github.com

まとめ

いいこと尽くめなので早くDottyメインの世界になって欲しいところです. が, まだ実装が終わっていなかったり, 互換性が切れるので既存ライブラリがそれから対応しなければならないことも考慮すると普及するのは少し先になりそうですね (暫くは2系がメジャーで, 3系 (Dotty) がいつからメインストリームになるかは明言されていないようです).

参考文献