what dotty fixes @ scala関西サミット

55
WHAT DOTTY FIXES

Upload: taisuke-oe

Post on 21-Jan-2018

1.734 views

Category:

Software


0 download

TRANSCRIPT

Page 1: What Dotty fixes @ Scala関西サミット

WHAT DOTTY FIXES

Page 2: What Dotty fixes @ Scala関西サミット

自己紹介麻植泰輔 / Taisuke OeTwitter @OE_uia最近Septeni Original, Inc. 技術アドバイザーエフ・コード 商品開発部顧問ScalaMatsuri 座長来年のScalaMatsuriは3日間! 3月16-18日です。

フルタイム職を辞めました

Page 3: What Dotty fixes @ Scala関西サミット

今日の話What Dotty �xes

Dottyが直した モノ

Page 4: What Dotty fixes @ Scala関西サミット

そもそもDOTTYって何?Scala3系で採用される新コンパイラMartin Odersky教授の研究室で主に開発が進んでいる最新versionは0.3-RC2pre-alphaなので、変なバグはまだ色々ありますScala 2.14 ~ 2.15系でmigrationするロードマップ(今の所)

Page 5: What Dotty fixes @ Scala関西サミット

DOTTYは何が嬉しいのか?Union Type, Implicit Function Typeなどを始めとしたより柔軟で強固な型システムscalacのバグ修正scalacの一見理解し難い仕様や制約の修正 <= 今日はこれの話をします

Page 6: What Dotty fixes @ Scala関西サミット

Q. 以下の式の戻り値はなんでしょうList(1).toSet()

Page 7: What Dotty fixes @ Scala関西サミット

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

なぜ?

Page 8: What Dotty fixes @ Scala関西サミット

解説List(1).toSet()

は以下のように展開される

scala -Xprint:typer

List.apply[Int](1).toSet[AnyVal].apply(())

どこから apply(()) が来た?

Page 9: What Dotty fixes @ Scala関西サミット

どこから APPLY が来たのか?toSetメソッドは、無引数でかつ 括弧なし で定義されている。

def toSet:Set[A]

括弧なしで定義された無引数メソッドは、toSet() のように括弧付きで呼び出すことはできない

=> apply メソッド呼び出しと解釈

Page 10: What Dotty fixes @ Scala関西サミット

自動的に補われる引数Set の apply[A](a:A):Boolean メソッドは1引数関数

Scalaのauto-tupling(後述)により、無引数関数に引数が渡されなかった場合、引数として () を補われてしまう

今回は () を引数として補っても、コンパイルが通る

()はSet(1)に含まれないため、apply関数の戻り値として false が返った

Page 11: What Dotty fixes @ Scala関西サミット

DOTTYになれば改善される?以下の2点について、Scala 2.12とDottyを比較

auto-tupling無引数メソッド呼び出しへの括弧付与

Page 12: What Dotty fixes @ Scala関西サミット

SCALA 2.12のAUTO-TUPLING関数の取りうる引数と、実際に渡した引数がマッチしなかった場合

渡した引数をTuple化してコンパイルできるかチェック

Page 13: What Dotty fixes @ Scala関西サミット

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オプション 警告

Page 14: What Dotty fixes @ Scala関西サミット

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)

Page 15: What Dotty fixes @ Scala関西サミット

SCALA 2.12における無引数メソッド呼び出しへの括弧付与既存の(特にJavaの)ライブラリとの互換性と、統一アクセス原則を両立するため

引数無しメソッドを空の引数リスト付きで定義されていた場合

メソッド呼び出しをする際に括弧をつけなくても自動的に括弧が追加される。

def getName():String

というメソッドは、getName と書いても良い。但し

def toSet:Set[A]

上のように空の引数リスト無しで定義されていた場合、(空の引数リストとしての)括弧は付与されない

Page 16: What Dotty fixes @ Scala関西サミット

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

Page 17: What Dotty fixes @ Scala関西サミット

ETA-EXPANSION

Page 18: What Dotty fixes @ Scala関西サミット

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 _

Page 19: What Dotty fixes @ Scala関西サミット

DOTTYのETA-EXPANSIONeta-expansionのための _ は廃止される。

値が要求されるところに引数有りメソッドを渡すと、自動でeta-expansionされる。

def double(a:Int):Int = a * 2

//型注釈がなくてもコンパイルが通る val doubleFunction = double

引数無しメソッドはeta-expansionを直接行う方法がなくなる。

def getName() = "name"

() => getName()

Page 20: What Dotty fixes @ Scala関西サミット

IMPLICITの型注釈

Page 21: What Dotty fixes @ Scala関西サミット

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

Page 22: What Dotty fixes @ Scala関西サミット

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

Page 23: What Dotty fixes @ Scala関西サミット

型クラスの依存関係

Page 24: What Dotty fixes @ Scala関西サミット

型クラスとは既存の型に対し、共通の振る舞いを後から定義する (アドホック多相を実現する)ためのデザインパターン

型クラス 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

Page 25: What Dotty fixes @ Scala関西サミット

DEPENDENT METHOD TYPE引数の型に依存して、メソッドのシグネチャ(のうち、多くの場合は戻り値の型)を変化させることができる

Page 26: What Dotty fixes @ Scala関西サミット

型クラス + 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

Page 27: What Dotty fixes @ Scala関西サミット

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]): ^

Page 28: What Dotty fixes @ Scala関西サミット

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))

Page 29: What Dotty fixes @ Scala関西サミット

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で解決する

Page 30: What Dotty fixes @ Scala関西サミット

型クラスインスタンスの再帰的導出

Page 31: What Dotty fixes @ Scala関西サミット

型クラスSHOWtrait Show[T] { def apply(t: T): String }

def show[T](t: T)(implicit s: Show[T]) = s(t)

Page 32: What Dotty fixes @ Scala関西サミット

再帰的なデータ構造に対する汎用的な型クラスインスタンス導出

次のような再帰的データ構造に対する、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

Page 33: What Dotty fixes @ Scala関西サミット

基底の型クラスインスタンス型クラスインスタンスを、要素型と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" } }

Page 34: What Dotty fixes @ Scala関西サミット

型クラスインスタンスの再帰的導出を試みる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) } } }

Page 35: What Dotty fixes @ Scala関西サミット

型クラスインスタンスの再帰的導出に失敗これまで定義した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)した、とはどういうことか?

Page 36: What Dotty fixes @ Scala関西サミット

暗黙展開の発散とは?scala> val l: List[Int] = Cons(0, Nil) scala> show(l)

showメソッドは暗黙のパラメーターとしてShow[List[Int]]型の値(型クラスインスタンス)を要求する。

Page 37: What Dotty fixes @ Scala関西サミット

暗黙展開の発散とは?すなわちコンパイル時に以下のように展開される。

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の型クラスインスタンスを要求するため、暗黙展開はループに陥る。

暗黙の展開が永遠に終わらない可能性を察知すると、コンパイラは先程のような「暗黙展開の発散」エラーを引き起こす。

Page 38: What Dotty fixes @ Scala関西サミット

本来LISTは有限の大きさのデータ型型クラスインスタンスの展開を(暗黙により)コンパイル時に行うと、型情報のみから展開することになる。 そのため値に依存してNilで展開を終えることができない。

実行時に(値の情報を基に)展開する方法はないだろうか?

Page 39: What Dotty fixes @ Scala関西サミット

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) } }

Page 40: What Dotty fixes @ Scala関西サミット

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)))

Page 41: What Dotty fixes @ Scala関西サミット

なぜ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に束縛され使いまわされるようになる

Page 42: What Dotty fixes @ Scala関西サミット

なぜ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))

Page 43: What Dotty fixes @ Scala関西サミット

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でも、同様に展開できる。

Page 44: What Dotty fixes @ Scala関西サミット

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) } }

Page 45: What Dotty fixes @ Scala関西サミット

余談[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の実装が変わる可能性がありそう

Page 46: What Dotty fixes @ Scala関西サミット

ENUMERATION

Page 47: What Dotty fixes @ Scala関西サミット

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("青") }

Page 48: What Dotty fixes @ Scala関西サミット

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)

Page 49: What Dotty fixes @ Scala関西サミット

DOTTYの新しいENUM新しい enum キーワードが

最新の0.3.0-RC-2で使用可

Enumeration(列挙型)、Algebraic Data Type(代数的データ型)等を便利に書くための糖衣構文

sealed class、companion objectとそのメンバ、ないしは子クラスに展開される

実装された

Page 50: What Dotty fixes @ Scala関西サミット

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") }

Page 51: What Dotty fixes @ Scala関西サミット

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)

Page 52: What Dotty fixes @ Scala関西サミット

その他のDOTTYで直るSCALA2系の制約lazy valによるdeadlock抽象型メンバーのshadowingtraitのコンストラクタ引数Function22制限

などなど

詳しくは を参照のことDotty Documentation

Page 53: What Dotty fixes @ Scala関西サミット

DOTTYへのMIGRATION

Page 54: What Dotty fixes @ Scala関西サミット

SCALAFIXScalaCenter主導で、scalametaを活用した というマイグレーションツールを鋭意開発中scala�x

Rewrite tool to prepare Scala 2.12 code for Dotty.

Dottyへのmigrationだけではなく、様々なmigrationで使われるかも?

sbtのメジャーバージョンアップ様々なScalaコンパイラfolkライブラリのメジャーバージョンアップ

Page 55: What Dotty fixes @ Scala関西サミット

最後にDottyはまだpre-alphaステージなので、詳細な実装などまだまだ大きく変わりえますが、

現段階でもScala2系の制約や問題があるのか、より深く理解する資料として優れています。

またDottyの先行実装をもとに、Scala2系へ何らかのbackportをされたものも少なくありません。

Dottyは勉強の題材として非常におもしろいので、ぜひお手元で遊んでみてください。