水底

ScalaとかC#とかk8sとか

続・Dottyによる変更点

以前以下のようなDottyに関する記事を書きました.

amaya382.hatenablog.jp

それから暫く経ち, 多くの更新が入ったため新しくまとめました. 公式ドキュメントだけでなくあちこちにちらばった情報を集めています. 過去記事と同じくあくまで真新しい機能のみに注目し, 理論的な部分や実装については深掘りしません. 概要のみなので, 個々の詳細は各見出しのリンク先を御覧ください.

f:id:amaya382:20170429233234p:plain

Dottyとは

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

Dotty github.com

Dottyの変更点

以下では現行のScalaをScala2と表記します. なお, 一部Dottyの新機能として公式で紹介されているものの, Scala2でも利用可能な機能があります. それについては見出しに [Scala2] を付けています.

なお, なんとなく関係してそうな機能順に並べていますが, 変更点が多岐に渡るため結構バラバラです.

Union Types

Union typesは A | B という形で AB から成る合併型を表します.

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 という形で AB から成る交差型を表します. 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 objectEnumeration クラスを利用する方法がありましたが, 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ではあくまでシングルトンしか扱えませんでしたが, アノテーションを使ってJavastatic に相当する静的な定義ができるようになります.

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

IntString を比較しているので常に 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 }
  • Phase fusionによるコンパイル高速化
  • インクリメンタルコンパイルが賢く
  • エラーメッセージが賢く
  • TASTY
    新しい中間ファイル. これを元にJVM/JS/Native向けのコードが吐かれる. マルチプラットフォーム化が容易になると思われる
  • Local optimisations
    final なフィールドにリフレクションでアクセスしない」という条件で利用可能な最適化オプション. -optimise というフラグで有効になる.
  • Dotty Linkerによる最適化
    • Dead Code Elimination
    • Automatic specialization
      @specialized による手動最適化が不要に
    • Convert classes to value classes
      条件を満たしていれば自動的にValue classが利用される
    • Eliminate virtual dispatch
      インライン化して呼び出しの最適化
    • 諸所の最適化でproguardを使った場合よりもバイトコードのサイズがコンパクトになる

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

Dottyとは別軸のものも含まれますが, 機能追加がされています.


まだ上記全ての機能が実装されたわけではなく, 上記以外に検討中の機能もあります (shapeless系, Non-nullable type, etc.). また, 一部機能はScala2にも実装が行われてい(ます|く予定です).

Dottyの使い方

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

参考

github.com

別の方法として, Webベース実行環境のScastieで手軽にDottyを試すこともできます.

scastie.scala-lang.org

Scala2との相互運用

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

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

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

Scala2からの移行

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

github.com

まとめ

いいこと尽くめなので早くDottyメインの世界になって欲しいところです. が, まだ実装が終わっていなかったり, 互換性が切れるのでどう普及していくかが気になるところです. ただ, ふんわりとしたロードマップが発表されたり (page 65 in What to leave implicit by Martin Odersky), バージョンも0.7まで上がってきたことを踏まえるとそう遠くない未来かもしれません.

f:id:amaya382:20170501160644p:plain

参考文献