What Dotty fixes @ Scala関西サミット

168 views

Published on

Scala関西サミット2017の発表資料です。
http://summit.scala-kansai.org/

Published in: Software
0 Comments
1 Like
Statistics
Notes
  • Be the first to comment

No Downloads
Views
Total views
168
On SlideShare
0
From Embeds
0
Number of Embeds
13
Actions
Shares
0
Downloads
0
Comments
0
Likes
1
Embeds 0
No embeds

No notes for slide

What Dotty fixes @ Scala関西サミット

  1. 1. WHAT DOTTY FIXES
  2. 2. 自己紹介 麻植泰輔 / Taisuke Oe Twitter @OE_uia 最近 Septeni Original, Inc. 技術アドバイザー エフ・コード 商品開発部顧問 ScalaMatsuri 座長 来年のScalaMatsuriは3日間! 3月16-18日です。 フルタイム職を辞めました
  3. 3. 今日の話 What Dotty xes Dottyが直した モノ
  4. 4. そもそもDOTTYって何? Scala3系で採用される新コンパイラ Martin Odersky教授の研究室で主に開発が進んでいる 最新versionは0.3-RC2 pre-alphaなので、変なバグはまだ色々あります Scala 2.14 ~ 2.15系でmigrationするロードマップ(今の所)
  5. 5. DOTTYは何が嬉しいのか? Union Type, Implicit Function Typeなどを始めとしたより柔軟で強固な型システム scalacのバグ修正 scalacの一見理解し難い仕様や制約の修正 <= 今日はこれの話をします
  6. 6. Q. 以下の式の戻り値はなんでしょう List(1).toSet()
  7. 7. 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 なぜ?
  8. 8. 解説 List(1).toSet() は以下のように展開される scala -Xprint:typer List.apply[Int](1).toSet[AnyVal].apply(()) どこから apply(()) が来た?
  9. 9. どこから APPLY が来たのか? toSetメソッドは、無引数でかつ 括弧なし で定義されている。 def toSet:Set[A] 括弧なしで定義された無引数メソッドは、toSet() のように括弧付きで呼び出すことはできない => apply メソッド呼び出しと解釈
  10. 10. 自動的に補われる引数 Set の apply[A](a:A):Boolean メソッドは1引数関数 Scalaのauto-tupling(後述)により、無引数関数に引数が渡されなかった場合、引数として () を補われてしまう 今回は () を引数として補っても、コンパイルが通る ()はSet(1)に含まれないため、apply関数の戻り値として false が返った
  11. 11. DOTTYになれば改善される? 以下の2点について、Scala 2.12とDottyを比較 auto-tupling 無引数メソッド呼び出しへの括弧付与
  12. 12. SCALA 2.12のAUTO-TUPLING 関数の取りうる引数と、実際に渡した引数がマッチしなかった場合 渡した引数をTuple化してコンパイルできるかチェック
  13. 13. 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オプション 警告
  14. 14. 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)
  15. 15. SCALA 2.12における無引数メソッド呼び出しへの括弧付与 既存の(特にJavaの)ライブラリとの互換性と、統一アクセス原則を両立するため 引数無しメソッドを空の引数リスト付きで定義されていた場合 メソッド呼び出しをする際に括弧をつけなくても自動的に括弧が追加される。 def getName():String というメソッドは、getName と書いても良い。但し def toSet:Set[A] 上のように空の引数リスト無しで定義されていた場合、(空の引数リストとしての)括弧は付与されない
  16. 16. 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
  17. 17. ETA-EXPANSION
  18. 18. 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 _
  19. 19. DOTTYのETA-EXPANSION eta-expansionのための _ は廃止される。 値が要求されるところに引数有りメソッドを渡すと、自動でeta-expansionされる。 def double(a:Int):Int = a * 2 //型注釈がなくてもコンパイルが通る val doubleFunction = double 引数無しメソッドはeta-expansionを直接行う方法がなくなる。 def getName() = "name" () => getName()
  20. 20. IMPLICITの型注釈
  21. 21. 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
  22. 22. 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
  23. 23. 型クラスの依存関係
  24. 24. 型クラスとは 既存の型に対し、共通の振る舞いを後から定義する (アドホック多相を実現する)ためのデザインパターン 型クラス 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
  25. 25. DEPENDENT METHOD TYPE 引数の型に依存して、メソッドのシグネチャ(のうち、多くの場合は戻り値の型)を変化させることができる
  26. 26. 型クラス + 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
  27. 27. 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]): ^
  28. 28. 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))
  29. 29. 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で解決する
  30. 30. 型クラスインスタンス の再帰的導出
  31. 31. 型クラスSHOW trait Show[T] { def apply(t: T): String } def show[T](t: T)(implicit s: Show[T]) = s(t)
  32. 32. 再帰的なデータ構造に対する汎用的な型クラスインスタンス 導出 次のような再帰的データ構造に対する、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
  33. 33. 基底の型クラスインスタンス 型クラスインスタンスを、要素型と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" } }
  34. 34. 型クラスインスタンスの再帰的導出を試みる 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) } } }
  35. 35. 型クラスインスタンスの再帰的導出に失敗 これまで定義した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)した、とはどういうことか?
  36. 36. 暗黙展開の発散とは? scala> val l: List[Int] = Cons(0, Nil) scala> show(l) showメソッドは暗黙のパラメーターとしてShow[List[Int]]型の値(型クラスインスタンス)を要求する。
  37. 37. 暗黙展開の発散とは? すなわちコンパイル時に以下のように展開される。 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の型クラスインスタンスを要求するため、暗黙展開はループに陥る。 暗黙の展開が永遠に終わらない可能性を察知すると、コンパイラは先程のような「暗黙展開の発散」エラーを引き起こす。
  38. 38. 本来LISTは有限の大きさのデータ型 型クラスインスタンスの展開を(暗黙により)コンパイル時に行うと、型情報のみから展開することになる。 そのため値に依存して Nilで展開を終えることができない。 実行時に(値の情報を基に)展開する方法はないだろうか?
  39. 39. 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) } }
  40. 40. 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)))
  41. 41. なぜ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に束縛され使いまわされるようになる
  42. 42. なぜ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))
  43. 43. 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でも、同様に展開できる。
  44. 44. 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) } }
  45. 45. 余談 [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の実装が変わる可能性があ りそう
  46. 46. ENUMERATION
  47. 47. 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("青") }
  48. 48. 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)
  49. 49. DOTTYの新しいENUM 新しい enum キーワードが 最新の0.3.0-RC-2で使用可 Enumeration(列挙型)、Algebraic Data Type(代数的データ型)等を便利に書くための糖衣構文 sealed class、companion objectとそのメンバ、ないしは子クラスに展開される 実装された
  50. 50. 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") }
  51. 51. 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)
  52. 52. その他のDOTTYで直るSCALA2系の制約 lazy valによるdeadlock 抽象型メンバーのshadowing traitのコンストラクタ引数 Function22制限 などなど 詳しくは を参照のことDotty Documentation
  53. 53. DOTTYへのMIGRATION
  54. 54. SCALAFIX ScalaCenter主導で、scalametaを活用した というマイグレーションツールを鋭意開発中scala x Rewrite tool to prepare Scala 2.12 code for Dotty. Dottyへのmigrationだけではなく、様々なmigrationで使われるかも? sbtのメジャーバージョンアップ 様々なScalaコンパイラfolk ライブラリのメジャーバージョンアップ
  55. 55. 最後に Dottyはまだpre-alphaステージなので、詳細な実装などまだまだ大きく変わりえますが、 現段階でもScala2系の制約や問題があるのか、より深く理解する資料として優れています。 またDottyの先行実装をもとに、Scala2系へ何らかのbackportをされたものも少なくありません。 Dottyは勉強の題材として非常におもしろいので、ぜひお手元で遊んでみてください。

×
Save this presentationTap To Close