what dotty fixes @ scala関西サミット
TRANSCRIPT
WHAT DOTTY FIXES
自己紹介麻植泰輔 / Taisuke OeTwitter @OE_uia最近Septeni Original, Inc. 技術アドバイザーエフ・コード 商品開発部顧問ScalaMatsuri 座長来年のScalaMatsuriは3日間! 3月16-18日です。
フルタイム職を辞めました
今日の話What Dotty �xes
Dottyが直した モノ
そもそもDOTTYって何?Scala3系で採用される新コンパイラMartin Odersky教授の研究室で主に開発が進んでいる最新versionは0.3-RC2pre-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-TUPLINGdef 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-EXPANSIONeta-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-EXPANSIONeta-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で解決する
型クラスインスタンスの再帰的導出
型クラスSHOWtrait 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抽象型メンバーのshadowingtraitのコンストラクタ引数Function22制限
などなど
詳しくは を参照のことDotty Documentation
DOTTYへのMIGRATION
SCALAFIXScalaCenter主導で、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は勉強の題材として非常におもしろいので、ぜひお手元で遊んでみてください。