水底

ScalaとかC#とかk8sとか

(※2018/3/17 新しい記事書きました) Dottyによる変更点と使い方

新しい記事を書きました. 本記事を包含します.

amaya382.hatenablog.jp


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

f:id:amaya382:20170429233234p:plain

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

そもそもDottyとは

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

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

// Dottyの交差型
(A & B) # T //=> Int & Double
// 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, 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
}

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も防ぐようです. スレッドセーフにする場合は @volatile が必須になる.

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

Option-less pattern matching (name-based pattern matcher) Dottyの新機能扱い ですが, もうScala2で使えます

以下の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しなくなります.

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)

Named type parameters

型引数に名前を付けることができるようになり, 併せて名前付き型引数の部分適用が可能になります

trait Map[type Key, type Value]
type IntMap = Map[Key = Int]

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 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 = ???

ツールの性能向上

コンパイラの性能向上

  • DOT計算ベースに刷新
    • Encoding type parameters
      高階型を表現するために型引数情報がメンバとして扱われるようになる. Named type parametersはこれをうまいこと利用してるみたい
      trait List[T]trait List { self => type T }
  • Phase fusionによる高速化
  • インクリメンタルコンパイルが賢く
  • エラーメッセージが賢く
  • TASTY
    新しい中間ファイル. これを元にJVM/JS/Native向けのコードが吐かれる. マルチプラットフォーム化が容易になると思われる
  • Dotty Linkerによる最適化
    • Dead Code Elimination
    • Automatic specialization
      @specialized による手動最適化が不要に
    • Convert classes to value classes
      条件を満たしていれば自動的にValue classが利用される
    • Eliminate virtual dispatch
      インライン化して呼び出しの最適化
    • 諸所の最適化でproguardを使った場合よりもバイトコードのサイズがコンパクトになる

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

その他の変更点

Macros 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.). また, 一部機能はScala2にも実装が行われてい(ます|く予定です).

Dottyの使い方

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

github.com

Scala2との相互運用

Scala2.11系でビルドされたライブラリを使うことができます. 例えばdependencyをこんな感じにすればok.

libraryDependencies += "org.json4s" % "json4s-native_2.11" % "3.5.1"

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

Scala2からの移行

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

github.com

まとめ

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

追記: ふんわりとしたロードマップは発表されていました (page 65 in What to leave implicit by Martin Odersky)

f:id:amaya382:20170501160644p:plain

参考文献