SlideShare a Scribd company logo
WHAT DOTTY FIXES
自己紹介
麻植泰輔 / Taisuke Oe
Twitter @OE_uia
最近
Septeni Original, Inc. 技術アドバイザー
エフ・コード 商品開発部顧問
ScalaMatsuri 座長
来年のScalaMatsuriは3日間! 3月16-18日です。
フルタイム職を辞めました
今日の話
What Dotty xes
Dottyが直した モノ
そもそもDOTTYって何?
Scala3系で採用される新コンパイラ
Martin Odersky教授の研究室で主に開発が進んでいる
最新versionは0.3-RC2
pre-alphaなので、変なバグはまだ色々あります
Scala 2.14 ~ 2.15系でmigrationするロードマップ(今の所)
DOTTYは何が嬉しいのか?
Union Type, Implicit Function Typeなどを始めとしたより柔軟で強固な型システム
scalacのバグ修正
scalacの一見理解し難い仕様や制約の修正 <= 今日はこれの話をします
Q. 以下の式の戻り値はなんでしょう
List(1).toSet()
A.
答え: false
scala> List(1).toSet()
<console>:12: warning: Adaptation of argument list by inserting () is deprecated:
signature: GenSetLike.apply(elem: A): Boolean
given arguments: <none>
after adaptation: GenSetLike((): Unit)
List(1).toSet()
^
res4: Boolean = false
なぜ?
解説
List(1).toSet()
は以下のように展開される
scala -Xprint:typer
List.apply[Int](1).toSet[AnyVal].apply(())
どこから apply(()) が来た?
どこから APPLY が来たのか?
toSetメソッドは、無引数でかつ 括弧なし で定義されている。
def toSet:Set[A]
括弧なしで定義された無引数メソッドは、toSet() のように括弧付きで呼び出すことはできない
=> apply メソッド呼び出しと解釈
自動的に補われる引数
Set の apply[A](a:A):Boolean メソッドは1引数関数
Scalaのauto-tupling(後述)により、無引数関数に引数が渡されなかった場合、引数として () を補われてしまう
今回は () を引数として補っても、コンパイルが通る
()はSet(1)に含まれないため、apply関数の戻り値として false が返った
DOTTYになれば改善される?
以下の2点について、Scala 2.12とDottyを比較
auto-tupling
無引数メソッド呼び出しへの括弧付与
SCALA 2.12のAUTO-TUPLING
関数の取りうる引数と、実際に渡した引数がマッチしなかった場合
渡した引数をTuple化してコンパイルできるかチェック
SCALA 2.12のAUTO-TUPLING
def f(ab:(A,B)) = ???
f(a,b) というメソッド呼び出しを f((a,b)) に変換する。
def g(a:A) = ???
g() というメソッド呼び出しを g(()) に変換する(!?)
※ ただし、auto-tuplingによる() の挿入はScala2.11以降はdeprecatedになっている
※ -Yno-adapted-args scalacオプション 無効化
※ -Ywarn-adapted-args scalacオプション 警告
DOTTYのAUTO-TUPLING
件のような () の挿入を行わない
関数のauto-tuplingを行う
例えばある関数オブジェクト
(x,y) => expr
がメソッドに渡されたとき、そのメソッドが一引数関数を要求している場合
{case (x,y) => expr}
に展開される
def f(a:Int,b:Int) = a + b
List(3,2,1).zipWithIndex.map(f)
SCALA 2.12における無引数メソッド呼び出しへの括弧付与
既存の(特にJavaの)ライブラリとの互換性と、統一アクセス原則を両立するため
引数無しメソッドを空の引数リスト付きで定義されていた場合
メソッド呼び出しをする際に括弧をつけなくても自動的に括弧が追加される。
def getName():String
というメソッドは、getName と書いても良い。但し
def toSet:Set[A]
上のように空の引数リスト無しで定義されていた場合、(空の引数リストとしての)括弧は付与されない
DOTTYにおける無引数メソッド呼び出しへの括弧付与
Dottyでは、Dotty外で定義された括弧なしの無引数メソッドのみ について、自動で括弧が付与される。
import java.util._
scala> new ArrayList[String]()
val res14: List[String] = []
scala> res14.size //コンパイル通る
前スライドの例は、コンパイルエラーとなる。
scala> def getName() = "name"
def getName(): String
scala> getName
-- Error: <console>:6:0 --------------------------------------------------------
6 |getName
|^^^^^^^
|missing arguments for method getName
ETA-EXPANSION
SCALA 2.12のETA-EXPANSION
eta-expansion: (Scalaにおいては)メソッドの関数オブジェクト化
以下の2つの方法でeta-expansionができる
1. FunctionN型の値が要求されているところにメソッドを渡す
2. メソッドに対し _ を明示的に呼び出す
def double(a:Int):Int = a * 2
List(1,2,3).map(double)
val doubleFunction:Int => Int = double
double _
DOTTYのETA-EXPANSION
eta-expansionのための _ は廃止される。
値が要求されるところに引数有りメソッドを渡すと、自動でeta-expansionされる。
def double(a:Int):Int = a * 2
//型注釈がなくてもコンパイルが通る
val doubleFunction = double
引数無しメソッドはeta-expansionを直接行う方法がなくなる。
def getName() = "name"
() => getName()
IMPLICITの型注釈
SCALA2.12 におけるIMPLICITの型注釈
暗黙の値に明示的に型注釈を書かない場合、 暗黙の値の探索に失敗してしまいコンパイルエラーとなる場合がある
//compile success
scala> :paste
object B {import A._ ; implicitly[Int]}
object A {implicit val a:Int = 1}
//COMPILE ERROR
scala> :paste
object B {import A._ ; implicitly[Int]}
object A {implicit val a = 1}
参考: Implicitには型注釈をつけましょう - OE_uia Tech Blog
DOTTYにおけるIMPLICITの注釈
Dottyでは、ローカルではない暗黙の値には型注釈が必須。つけないとコンパイルエラーになる。
scala> implicit val a = 1
-- Error: <console>:4:13 -------------------------------------------------------
4 |implicit val a = 1
|^^^^^^^^^^^^^^^^^^
|type of implicit definition needs to be given explicitly
//暗黙のローカル変数なら型注釈が不要
scala> def f = {implicit val a = 1;a}
def f: Int
型クラスの依存関係
型クラスとは
既存の型に対し、共通の振る舞いを後から定義する (アドホック多相を実現する)ためのデザインパターン
型クラス Semigroup
trait Semigroup[A]{
def append(a1:A,a2:A):A
}
object Semigroup{
implicit val intGroup:Semigroup[Int] = new Semigroup[Int]{
def append(a1:Int,a2:Int):Int = a1 + a2
}
}
object SemigroupSyntax{
def append[A](a1:A, a2:A)(implicit S:Semigroup[A]):A = S.append(a1,a2)
}
import SemigroupSyntax._
append(1,2) // 3
DEPENDENT METHOD TYPE
引数の型に依存して、メソッドのシグネチャ(のうち、多くの場合は戻り値の型)を変化させることができる
型クラス + DEPENDENT METHOD TYPEの例
Measurableという型クラスを使って、Dependent Method Typeを活用する例
trait Measurable[A]{
type Size
def sizeOf(a:A):Size
}
object Measurable{
implicit val intSize:Measurable[Int] = new Measurable[Int]{
type Size = Int
def sizeOf(i:Int):Int = i
}
implicit def seqSize[A]:Measurable[Seq[A]] = new Measurable[Seq[A]]{
type Size = Int
def sizeOf(s:Seq[A]):Int = s.size
}
}
object MeasurableSyntax{
def measure[A](a:A)(implicit M:Measurable[A]):M.Size = M.sizeOf(a)
}
import MeasurableSyntax._
measure(Seq(1,2,3)) // 3
measure(1) // 1
SCALA2.12で型クラスを組み合わせる
同じ引数リスト内の引数を参照できない
scala> def sumSizes[A](a1:A,a2:A)(implicit M:Measurable[A], S:Semigroup[M.Size]):
| S.append(M.measure(a1),M.measure(a2))
<console>:32: error: illegal dependent method type:
parameter may only be referenced in a subsequent parameter section
def sumSizes[A](a1:A,a2:A)(implicit M:Measurable[A], S:Semigroup[M.Size]):
^
SCALA2.12で型クラスを組み合わせる
AUXパターン
型メンバを型パラメーターへマッピングすることで、暗黙のパラメーターが持つ型パラメーター同士で依存させることができる
trait Measurable[A]{
type Size
def sizeOf(a:A):Size
}
//ここまで同じ
object Measurable{
type Aux[A0,B0] = Measurable[A0]{type Size = B0}
implicit val intAux:Measurable.Aux[Int,Int] = new Measurable[Int]{
type Size = Int
def sizeOf(i:Int):Int = i
}
}
def sumSizes[A,Size](a1:A,a2:A)(implicit M:Measurable.Aux[A,Size], S:Semigroup[Size
S.append(M.sizeOf(a1),M.sizeOf(a2))
DOTTYで型クラスを組み合わせる
Dottyでは、同じ引数リストの中でも依存関係を作れる
object SemigroupMeasurableSyntax {
def sumSizes[A](a1:A,a2:A)(implicit M:Measurable[A], S:Semigroup[M.Size]):M.Size
S.append(M.sizeOf(a1),M.sizeOf(a2))
}
詳細: AuxパターンをDottyで解決する
型クラスインスタンス
の再帰的導出
型クラスSHOW
trait Show[T] {
def apply(t: T): String
}
def show[T](t: T)(implicit s: Show[T]) = s(t)
再帰的なデータ構造に対する汎用的な型クラスインスタンス
導出
次のような再帰的データ構造に対する、Show 型クラスのインスタンスを導出したい
sealed trait List[+T]
case class Cons[T](hd: T, tl: List[T]) extends List[T]
sealed trait Nil extends List[Nothing]
case object Nil extends Nil
再帰的に導出することで解決できないか?
出典: Scala Exercise - Shapeless
基底の型クラスインスタンス
型クラスインスタンスを、要素型とNilに対し定義する。
object Show {
implicit def showInt: Show[Int] = new Show[Int] {
def apply(t: Int) = t.toString
}
implicit def showNil: Show[Nil] = new Show[Nil] {
def apply(t: Nil) = "Nil"
}
}
型クラスインスタンスの再帰的導出を試みる
List及びConsに対する型クラスインスタンスは、データ構造に沿って再帰的に、暗黙のパラメーターを展開し導出する
object Show{
implicit def showCons[T](implicit st: Show[T], sl: Show[List[T]]): Show[Cons[T]] =
def apply(t: Cons[T]) = s"Cons(${show(t.hd)(st)}, ${show(t.tl)(sl)})"
}
implicit def showList[T](implicit sc: Show[Cons[T]]): Show[List[T]] = new Show[
def apply(t: List[T]) = t match {
case n: Nil => show(n)
case c: Cons[T] => show(c)(sc)
}
}
}
型クラスインスタンスの再帰的導出に失敗
これまで定義したShowのインスタンス及び導出用の関数を駆使して、 要素数1のListの型クラスインスタンスの導出を試みる。
scala> val l: List[Int] = Cons(0, Nil)
l: List[Int] = Cons(0,Nil)
scala> show(l)
<console>:20: error: diverging implicit expansion for type Show[Cons[Int]]
starting with method showList in object Show
show(res0)
^
「showListからはじまる暗黙展開(implicit expansion)」が発散(diverging)した、とはどういうことか?
暗黙展開の発散とは?
scala> val l: List[Int] = Cons(0, Nil)
scala> show(l)
showメソッドは暗黙のパラメーターとしてShow[List[Int]]型の値(型クラスインスタンス)を要求する。
暗黙展開の発散とは?
すなわちコンパイル時に以下のように展開される。
scala> show(l){
//以下、コンパイル時に暗黙の引数として渡される値
/* 1 */ showList{
/* 2 */ showCons(
/* 3 */ showInt, showList{
/* 4 */ showCons(/* ... */)
})
}
}
1. showメソッドがListの型クラスインスタンスを要求する
2. Listの型クラスインスタンスを、Consの型クラスインスタンスから導出する
3. Consの型クラスインスタンスは、headに相当するIntの型クラスインスタンスと、tailに相当するListの型クラスインスタンスから導
出する
4. (先のtailに相当する)Listの型クラスインスタンスは、やはりConsの型クラスインスタンスから導出可能
ここで再び1のステップのようにListの型クラスインスタンスを要求するため、暗黙展開はループに陥る。
暗黙の展開が永遠に終わらない可能性を察知すると、コンパイラは先程のような「暗黙展開の発散」エラーを引き起こす。
本来LISTは有限の大きさのデータ型
型クラスインスタンスの展開を(暗黙により)コンパイル時に行うと、型情報のみから展開することになる。 そのため値に依存して
Nilで展開を終えることができない。
実行時に(値の情報を基に)展開する方法はないだろうか?
SCALA 2.12で暗黙展開の発散を防ぐ方法
Shapelessの Lazy 型コンストラクタは、型クラスインスタンスの展開の殆どを実行時に遅延させる。
Lazyを使うと、型クラスインスタンス導出の定義は以下のようになる。
implicit def showCons[T](implicit st: Show[T], sl: Lazy[Show[List[T]]]): Show[Cons
def apply(t: Cons[T]) = s"Cons(${show(t.hd)(st)}, ${show(t.tl)(sl.value)})"
}
implicit def showList[T](implicit sc: Lazy[Show[Cons[T]]]): Show[List[T]] = new
def apply(t: List[T]) = t match {
case n: Nil => show(n)
case c: Cons[T] => show(c)(sc.value)
}
}
SCALA 2.12で暗黙展開の発散を防ぐ方法
Lazyを使うことで暗黙展開の発散を防げる
scala> val l: List[Int] = Cons(1, Cons(2, Cons(3, Nil)))
scala> show(l)
res2: String = Cons(1, Cons(2, Cons(3, Nil)))
なぜLAZY型コンストラクタで暗黙展開の発散が防げるのか?
Lazyの役割は、大きく分けて2つ。
1. マクロにより、暗黙のパラメーターの展開を以下のように修正する
val l: List[Int] = Cons(0, Nil)
show(l){
//以下、暗黙の引数として渡される値
lazy val list:Show[List[Int]] = showList(Lazy(cons))
lazy val cons:Show[Cons[Int]] = showCons(showInt, Lazy(list))
list
}
同じ型に対する型クラスインスタンスがlazy valに束縛され使いまわされるようになる
なぜLAZY型コンストラクタで暗黙展開の発散が防げるのか?
1. 以下のようなデータ構造により、 Lazy.applyに渡された値(型クラスインスタンス)の評価を遅延する (※ 実際のコードより簡略
化しています)
trait Lazy[T]{val value:T}
object Lazy{
def apply[T](t: => T):Lazy[T] =
new Lazy[T]{
lazy val value = t
}
}
これにより、型クラスインスタンスの展開は (コンパイル時ではなく)実行時に行われるようになる。 (もし展開がループに陥る場合は
StackOverFlowを引き起こすことに注意)
lazy val list:Show[List[Int]] = showList(Lazy(cons))
LAZYによる型クラスインスタンスの実行時展開
val l:List[Int] = Cons(0,Nil)
show(l)
この show を、擬似的にインライン展開すると:
//Listの型クラスインスタンスへの委譲
showList.apply(Cons(0,Nil))
//Listの型クラスインスタンスから、Consの型クラスインスタンスへ委譲
showCons.apply(Cons(0,Nil))
//Consの型クラスインスタンスから、IntとListの型クラスインスタンスへ委譲
s"Cons(${showInt.apply(0)}, ${showList.apply(Nil)})"
//Listの型クラスインスタンスから、Nilの型クラスインスタンスへ委譲
s"Cons(${showInt.apply(0)}, ${showNil.apply(Nil)})"
発散せず、すべて基底の型クラスインスタンスに委譲できた。
要素数2以上のListでも、同様に展開できる。
DOTTYで暗黙展開の発散を防ぐ方法
implicit by-name parameterにより、暗黙展開の発散を防ぐ
ShapelessのLazyと異なりマクロを使わないが、型クラスインスタンスを内部でlazy valに束縛する点では同じ
現時点では、implicit by-name parameterとDependent Method Typeを(実装上の都合で)同時に使えない
implicit def showCons[T](implicit st: Show[T], sl: => Show[List[T]]): Show[Cons[
def apply(t: Cons[T]) = s"Cons(${show(t.hd)(st)}, ${show(t.tl)(sl)})"
}
implicit def showList[T](implicit sc: => Show[Cons[T]]): Show[List[T]] = new Show
def apply(t: List[T]) = t match {
case n: Nil => show(n)
case c: Cons[T] => show(c)(sc)
}
}
余談
[WIP] Implementation of byname implicits with recursive dictionaries. by milessabin · Pull Request #6050 · scala/scala
Scala 2.12 系でもimplicit by-name parameterが入るかも!(現在WIP)
上記に関連してDottyのissue上でも議論は続いており、今後Dottyのimplicit by-name parameterの実装が変わる可能性があ
りそう
ENUMERATION
SCALA 2.12のENUMERATION
拡張しにくい
Color.Value が汚い
object Color extends Enumeration{
val Red,Yellow,Green = Value
}
def show(color:Color.Value):Unit = color match{
case Color.Red => println("赤")
case Color.Yellow => println("黃")
case Color.Green => println("青")
}
SCALA2.12におけるENUMERATIONの代替: SEALEDによる直和型(SUM TYPE)
拡張は容易だが、冗長。
特に、enumerationの一覧などが欲しい場合など自前で実装しないといけない。
sealed trait Color{def name:String}
object Color{
case object Red extends Color{val name = "赤"}
case object Yellow extends Color{val name = "黃"}
case object Green extends Color{val name = "青"}
}
def show(color:Color):Unit = println(color.name)
DOTTYの新しいENUM
新しい enum キーワードが
最新の0.3.0-RC-2で使用可
Enumeration(列挙型)、Algebraic Data Type(代数的データ型)等を便利に書くための糖衣構文
sealed class、companion objectとそのメンバ、ないしは子クラスに展開される
実装された
ENUMを使ったENUMERATIONの例
enum Color{
case Red,Yellow,Green
}
... は以下に展開される
sealed abstract class Color extends scala.Enum
object Color {
private val $values = new scala.runtime.EnumValues[Color]
def enumValue: Map[Int, Color] = $values.fromInt
def enumValueNamed: Map[String, Color] = $values.fromName
def enumValues: Iterable[Color] = $values.values
def $new(tag: Int, name: String): Color = new Color {
def enumTag: Int = tag
override def toString: String = name
$values.register(this)
}
final val Red: Color = $new(0, "Red")
final val Yellow: Color = $new(1, "Yellow")
final val Green: Color = $new(2, "Green")
}
ENUM によるENUMERATIONは拡張も容易
enum Color(val name:String){
case Red extends Color("赤")
case Yellow extends Color("黃")
case Green extends Color("青")
}
def show(color:Color):Unit = println(color.name)
その他のDOTTYで直るSCALA2系の制約
lazy valによるdeadlock
抽象型メンバーのshadowing
traitのコンストラクタ引数
Function22制限
などなど
詳しくは を参照のことDotty Documentation
DOTTYへのMIGRATION
SCALAFIX
ScalaCenter主導で、scalametaを活用した というマイグレーションツールを鋭意開発中scala x
Rewrite tool to prepare Scala 2.12 code for Dotty.
Dottyへのmigrationだけではなく、様々なmigrationで使われるかも?
sbtのメジャーバージョンアップ
様々なScalaコンパイラfolk
ライブラリのメジャーバージョンアップ
最後に
Dottyはまだpre-alphaステージなので、詳細な実装などまだまだ大きく変わりえますが、
現段階でもScala2系の制約や問題があるのか、より深く理解する資料として優れています。
またDottyの先行実装をもとに、Scala2系へ何らかのbackportをされたものも少なくありません。
Dottyは勉強の題材として非常におもしろいので、ぜひお手元で遊んでみてください。

More Related Content

What Dotty fixes @ Scala関西サミット