Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

Scala 3 勉強会#3 Context Abstractions

Tomoki Mizogami
October 15, 2021
51

Scala 3 勉強会#3 Context Abstractions

社内で Scala 3 勉強会を実施した際の資料(公開用)。

目次
・Given インスタンス
・Using 句
・Extension Methods
・Implicit Conversions
・Context Functions

Tomoki Mizogami

October 15, 2021
Tweet

Transcript

  1. #2の振り返り • 具象クラスを定義すると、自動的に Constructor Proxy (コンパニオンオブジェ クトと apply メソッド) が生成されるようになった

    • Scala 3 では、インデントでブロックを表せるようになった ◦ いろんなルールがある • 型引数のワイルドカードが _ から ? に変更になった
  2. やること • 引き続き、New in Scala 3 [1] • Contextual Abstractions

    ◦ Given インスタンス ◦ Using 句 ◦ 拡張メソッド ◦ 暗黙の型変換 ◦ Context function ◦ 型クラスの導出 ◦ Multiversal Equality • 練習問題
  3. Context Abstractions • これまで implicit が持っていた機能が、それぞれの用途に合わせた機能に分 割された 型変換 implicit def

    intToBool(i: Int): Boolean = ??? 拡張メソッド implicit class StringOps(v: String) { def hoge() = ??? } パラメータ implicit ec: ExecutionContext 型クラスのモデリング implicit object intOrd extends Ordering[Int] { def compare(x: Int, y:Int) = } implicit
  4. • これまで implicit が持っていた機能が、それぞれの用途に合わせた機能に分 割された Context Abstractions 型変換 given Conversion[Int,

    Boolean] with def apply(v: Int): Boolean = ??? 拡張メソッド extension [T](v: String) def hoge() = ??? パラメータ using ExecutionContext 型クラスのモデリング given intOrd: Ordering[Int] with def compare(x: Int, y: Int) = ???
  5. • 型クラスの実装やコンテキストの定義等に使われる [2] Given インスタンス given localDateOrdering: Ordering[LocalDate] with def

    compare(x: LocalDate, y: LocalDate): Int = x compareTo y Scala 2 Scala 3 implicit def localDateOrdering = new Ordering[LocalDate] { def compare(x: LocalDate, y: LocalDate): Int = x compareTo y } given ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global Scala 2 Scala 3 implicit val ec: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
  6. • 型クラスとは? ◦ 型パラメータ [A], [F[_]] を持つようなトレイト、クラスのこと ◦ インスタンスを実装することにより、そのクラスの性質を特定の型に持たせることができる ◦

    例)Ordering, Writes / Reads (play-json), Semigroup, Monoid, Functor, Monad • 例:Ordering (順序の性質) • 例:Monoid (加算と単位元を持つ、という性質) Given インスタンス:型クラス trait Monoid[A]: def add(x: A, y: A): A def unit: A trait Ordering[A]: def compare(x: A, y: A): Int
  7. • Entity クラス (独自クラス) の順序を定義する ◦ id 昇順 Given インスタンス:型クラスの実装

    scala> case class Entity(id: Long) // defined case class Entity scala> val list = List(Entity(3L), Entity(1L), Entity(4L), Entity(2L)) val list: List[Entity] = List(Entity(3), Entity(1), Entity(4), Entity(2)) scala> list.sorted 1 |list.sorted | ^ |No implicit Ordering defined for B | ... scala> given entityOrdering: Ordering[Entity] with | def compare(x: Entity, y: Entity) = | summon[Ordering[Long]].compare(x.id, y.id) | // defined object entityOrdering scala> list.sorted val res2: List[Entity] = List(Entity(1), Entity(2), Entity(3), Entity(4))
  8. • Monoid のインスタンスを定義する Given インスタンス:型クラスの実装 scala> given intMonoid: Monoid[Int] with

    | def add(x: Int, y: Int): Int = x + y | def unit: Int = 0 | // defined object intMonoid scala> given optionMonoid[A](using m: Monoid[A]): Monoid[Option[A]] with | def add(x: Option[A], y: Option[A]): Option[A] = | (x, y) match | case (Some(x1), Some(y1)) => Some(m.add(x1, y1)) | case (Some(x1), None) => Some(x1) | case (None, Some(y1)) => Some(y1) | case _ => None | def unit: Option[A] = None | // defined class optionMonoid scala> summon[Monoid[Option[Int]]].add(Option(4), Option(2)) val res1: Option[Int] = Some(6)
  9. • given の変数名は省略可能 • using でコンテキストを渡すこともできる (後述) given Ordering[Entity] with

    def compare(x: Entity, y: Entity) = summon[Ordering[Long]].compare(x.id, y.id) Given インスタンス:型クラスの実装 given [A](using m: Monoid[A]): Monoid[Option[A]] with ...
  10. • 例)ExecutionContext の渡し方 ◦ 簡略化のために Implicits.global を使用 • 変数名は省略可能 given

    global: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global Given インスタンス:コンテキストの定義 given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global
  11. • given インスタンスは import XXX._ ではインポートできない • 代わりに、import XXX.given でインポートする

    [3] • 特定の型のインスタンスのみインポートすることも可能 • given インスタンスを除くメンバーをインポートする場合 import XXX.* scala> import chapter3.MonoidInstances.given scala> summon[chapter3.Monoid[List[Int]]].unit val res0: List[Int] = List() scala> import chapter3.MonoidInstances.given chapter3.Monoid[List[Int]] scala> import chapter3.MonoidInstances.{ given chapter3.Monoid[List[?]], given chapter3.Monoid[Option[?]] } Given インスタンス:given インスタンスのインポート
  12. Using 句 • 渡されたコンテキスト (Given インスタンス) を利用する [4] ◦ Ordering

    を使った例 scala> def max[T](x: T, y: T)(using ord: Ordering[T]) = | if ord.compare(x, y) < 0 then y else x | def max[T](x: T, y: T)(using ord: Ordering[T]): T scala> max(1, 2) val res0: Int = 2 scala> max("abcdefg", "hijklmn") val res1: String = hijklmn
  13. Using 句 • 渡されたコンテキスト (Given インスタンス) を利用する ◦ Monoid を使った例

    scala> def sum[T: Monoid](list: List[T])(using ev: Monoid[T]) = | list.foldLeft(ev.unit)(ev.add) def sum [T] (list: List[T]) (implicit evidence$1: chapter3.Monoid[T], ev: chapter3.Monoid[T]): T scala> import chapter3.MonoidInstances.given // サンプルコードの given インスタンスをインポート scala> sum(List(1, 2, 3, 4, 5)) val res0: Int = 15 scala> sum(List(Option(1), Option(2), None, Option(3), Option(4), Option(5))) val res1: Option[Int] = Some(15)
  14. scala> import scala.concurrent.{ Future, ExecutionContext } scala> def usingExecutionContext(using ExecutionContext)

    = | Future { | (1 to 10).foreach { i => | Thread.sleep(1000) | println(i) | } | } def usingExecutionContext (using x$1: concurrent.ExecutionContext): scala.concurrent.Future[Unit] scala> given ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global lazy val given_ExecutionContext: concurrent.ExecutionContext scala> usingExecutionContext val res2: scala.concurrent.Future[Unit] = Future(<not completed>) 1 2 3 4 5 6 7 8 9 10 Using 句 • 渡されたコンテキスト (暗黙のパラメータ) を利用する
  15. • summon メソッド [5] により、インスタンスを取得できる scala> import chapter3.Monoid scala> import

    chapter3.MonoidInstances.given scala> summon[Monoid[Int]].unit val res1: Int = 0 scala> summon[Monoid[String]].unit val res2: String = "" scala> summon[Monoid[List[Int]]].add(List(1, 2, 3), List(4, 5, 6)) val res3: List[Int] = List(1, 2, 3, 4, 5, 6) Using 句: インスタンスの利用
  16. • 任意の型にメソッドを生やすことのできる素晴らしい機能 [6] 拡張メソッド extension [T](xs: List[T]) def sumBy[U: Numeric](f:

    T => U): U = xs.map(f).sum Scala 2 Scala 3 implicit class ListOps[T](list: List[T]) { def sumBy[U: Numeric](f: T => U) = list.map(f).sum } scala> case class Entity(id: Long, score: Int) // defined case class Entity scala> val list = List(Entity(3L, 100), Entity(1L, 90), Entity(4L, 40), Entity(2L, 50)) val list: List[Entity] = List(Entity(3,100), Entity(1,90), Entity(4,40), Entity(2,50)) scala> list.sumBy(_.score) val res1: Int = 280
  17. • これまで (Scala 2 の暗黙クラス) は import しないと使えなかったが Scala 3

    では以下の条件のいずれかが成り立つとき、メソッドを参照可能 a. スコープ内で定義、継承またはインポートされているとき b. 参照中の特定の given インスタンスのメンバーとして与えられているとき c. 型 r の暗黙のスコープで定義されているとき (r.m として参照可能) d. 型 r の暗黙のスコープの特定の given インスタンスで定義されているとき (r.m として参照可 能) 拡張メソッド
  18. • a) スコープ内で定義、継承またはインポートされているとき 拡張メソッド case class Entity(id: Long, scoreA: Int,

    scoreB: Int, scoreC: Int) trait EntityOps: extension (entity: Entity) def sum: Int = entity.scoreA + entity.scoreB + entity.scoreC extension (entity: Entity) def avg: Double = // extension method `sum` defined in same scope entity.sum / 3 object EntityOpsExt extends EntityOps: extension (entity: Entity) def avgAsInt: Int = // extension method `avg` brought into scope via inheritance from EntityOps entity.avg.toInt trait ListEntityOps: import EntityOpsExt.{ avg => avgScore } // brings `avg` into scope as `avgScore` extension (entities: List[Entity]) def avg: Double = entities.nonEmpty match // extension methods `avg` imported and thus in scope case true => entities.map(_.avgScore).sum / entities.length case false => 0.0
  19. • b) 参照中の特定の given インスタンスのメンバーとして与えられているとき 拡張メソッド scala> given ops1: chapter3.ExtensionMethodsRules.Rule1.EntityOps()

    // defined object ops1 scala> val entity = chapter3.ExtensionMethodsRules.Rule1.Entity(1L, 100, 80, 70) val entity: chapter3.ExtensionMethodsRules.Rule1.Entity = Entity(1,100,80,70) scala> entity.avg val res0: Double = 83.0
  20. • c) 型 r の暗黙のスコープで定義されているとき 拡張メソッド scala> case class Entity(id:

    Long, scoreA: Int, scoreB: Int, scoreC: Int) | object Entity: | extension (entity: Entity) | def avg: Double = | (entity.scoreA + entity.scoreB + entity.scoreC) / 3 | extension (entities: List[Entity]) | def avg: Double = | entities.nonEmpty match | case true => entities.map(_.avg).sum / entities.length | case false => 0.0 | // defined case class Entity // defined object Entity scala> val list = List(Entity(1L, 100, 80, 50), Entity(2L, 50, 60, 70), Entity(3L, 100, 100, 100)) val list: List[chapter3.ExtensionMethodsRules.Rule3.Entity] = List(Entity(1,100,80,50), Entity(2,50,60,70), Entity(3,100,100,100)) scala> list.avg val res0: Double = 78.66666666666667
  21. • d) 型rの暗黙のスコープの特定のgivenインスタンスで定義されているとき 拡張メソッド scala> trait EntityOps: | extension (entity:

    Entity) | def avg: Double = | (entity.scoreA + entity.scoreB + entity.scoreC) / 3 | | extension (entities: List[Entity]) | def avg: Double = | entities.nonEmpty match | case true => entities.map(_.avg).sum / entities.length | case false => 0.0 | | case class Entity(id: Long, scoreA: Int, scoreB: Int, scoreC: Int) | object Entity: | given ops: EntityOps() | scala> val list = List(Entity(1L, 100, 80, 50), Entity(2L, 50, 60, 70), Entity(3L, 100, 100, 100)) val list: List[chapter3.ExtensionMethodsRules.Rule4.Entity] = List(Entity(1,100,80,50), Entity(2,50,60,70), Entity(3,100,100,100)) scala> list.avg val res0: Double = 78.66666666666667
  22. • summon[Monoid[List[Int]]].add(List(1, 2, 3), List(4, 5, 6)) と書いていたところを List(1, 2,

    3) |+| List(4, 5, 6)) みたいな風にできそう 拡張メソッド: 例 trait Monoid[T]: def add(x: T, y: T): T def unit: T extension (x: T) def |+|(y: T): T = x.add(y) scala> import chapter3.MonoidInstances.given scala> 1 |+| 3 val res0: Int = 4 scala> List(1, 2, 3) |+| List(4, 5, 6) val res1: List[Int] = List(1, 2, 3, 4, 5, 6) scala> Option(4) |+| Option(6) val res2: Option[Int] = Some(10) 定義に追加
  23. 暗黙の型変換 • コンパイラが型 T から型 U への型変換を暗黙的にやってくれる機能 [7] • Conversion

    クラスの Given インスタンスで定義。書きやすくなった scala> import scala.language.implicitConversions scala> case class A(value: Int) // defined case class A scala> given Conversion[Int, A] with | def apply(v: Int): A = new A(v) | // defined object given_Conversion_Int_A scala> val hoge: A = 3 val hoge: A = A(3) Scala 2 Scala 3 scala> import scala.language.implicitConversions scala> case class A(value: Int) // defined case class A scala> implicit def intToA(v: Int): A = A(v) def intToA(v: Int): A scala> val hoge: A = 3 val hoge: A = A(3)
  24. 暗黙の型変換: Conversion クラス • Conversion の定義 [8] • Function1 (関数型

    T => U) を継承しているので、関数としても書ける abstract class Conversion[-T, +U] extends Function1[T, U]: /** Convert value `x` of type `T` to type `U` */ def apply(x: T): U given Conversion[Long, A] = v => A(v.toInt)
  25. 暗黙の型変換: 型変換されるケース • 以下のケースで暗黙の型変換が実行される a. 式 e の型が T であり、T

    が e の期待される型 U でない場合 b. 型 T の式 e の getter e.m に関して、T がメンバー m を定義していない場合 c. 型 T の式 e のメソッド適用 e.m(args) に関して、T が m を定義しているがいずれも引数に 適用できない場合
  26. • a) 式 e の型が T であり、T が e の期待される型

    U でない場合 ◦ 例)式の型は Int だが、期待される型が A である • b) 型 T の式 e の e.m に関して、T がメンバー m を定義していない場合 ◦ 例)Int が A のメンバー value を定義していない 暗黙の型変換: 型変換されるケース scala> val hoge: A = 3 val hoge: A = A(3) scala> 3.value val res0: Int = 3
  27. 暗黙の型変換: 型変換されるケース • c) 型 T の式 e のメソッド適用 e.m(args)

    に関して、T が m を定義している がいずれも引数に適用できない場合 ◦ 例)B のメソッド add (引数 Long 型) に String 型の値を渡した (C の add を適用) scala> case class B(value: Long) { self => | def add(rhs: Long): B = self.copy(value + rhs) | } // defined case class B scala> case class C(value: String) { self => | def add(rhs: String): C = self.copy(value + rhs) | } // defined case class C scala> given Conversion[B, C] = b => C(b.value.toString) lazy val given_Conversion_B_C: Conversion[B, C] scala> import scala.language.implicitConversions scala> B(3).add("5") val res2: C = C(35)
  28. • Context Function は、コンテキストのパラメータのみを持つ関数 [9] ◦ そのような型を Context Function 型と呼び、以下のように

    A ?=> B で表記する Context Function scala> trait Context // defined trait Context scala> type ContextFunction[A] = Context ?=> A // defined alias type ContextFunction[A] = (Context) ?=> A
  29. Context Function • 関数適用は暗黙的にも、明示的にも行うことができる scala> def func(x: Int): Context ?=>

    Int = x * 2 def func(x: Int): (Context) ?=> Int scala> val ctx = new Context {} val ctx: Context = anon$1@3dd76768 scala> func(3) -- Error: 1 |func(3) | ^ |no implicit argument of type Context was found for parameter of (Context) ?=> Int scala> func(3)(using ctx) val res0: Int = 6 scala> given Context = ctx lazy val given_Context: Context scala> func(3) val res1: Int = 6
  30. Context Function: Context Function リテラル • パラメータのコンテキストは、以下のように書くことにより Context Function リ

    テラルに変換可能 scala> import chapter3.Monoid scala> def zero[A]: Monoid[A] ?=> A = (m: Monoid[A]) ?=> m.unit def zero[A] => (chapter3.Monoid[A]) ?=> A scala> import chapter3.MonoidInstances.given scala> zero[String] val res0: String = "" scala> zero[Option[Int]] val res1: Option[Int] = None
  31. Context Function: なにが嬉しいか • 正直まだわからない 🤔 ◦ 個人的には、コンテキストの引数を型に落とし込めるのが良いかも • 慣れるために、Html

    テンプレート生成器を Builder パターンで作成してみる val template = html { div { p("Hello") img("/assets/images/hoge.jpg") } } println(template.toHtmlString) <html> <div> <p>Hello</p> <img src="/assets/images/hoge.jpg" /> </div> </html> 実行すると
  32. Context Function: Html テンプレート • コンテキストには、親タグからの情報(インデント)を渡すようにする • Tag: Html タグの全般を表すトレイト

    import scala.collection.mutable.ArrayBuffer import Tag.Indent trait Tag: val childs = new ArrayBuffer[Tag] // 子タグ def add(t: Tag): Unit = childs += t // 子タグの追加 def toHtmlString: Indent ?=> String // テンプレートへの変換 object Tag: opaque type Indent = Int // インデントを表現する型(Opaque Type については後日。Tagged Type みたいなもの) object Indent: def apply(v: Int): Indent = v extension (lhs: Indent) def toSpace: String = " " * lhs // インデントをスペースにする def +(rhs: Int): Indent = lhs + rhs // Int との加算をサポート
  33. Context Function: Html テンプレート • Html: <html> を表すクラス ◦ toHtmlString

    メソッドでは、子タグにコンテキストとしてインデントを渡している ◦ html メソッドでは、子タグを生成するブロック init にコンテキスト Html を渡している class Html extends Tag: def toHtmlString = (indent: Indent) ?=> val childIndent = indent + 1 indent.toSpace + "<html>" + childs.map(child => child.toHtmlString(using childIndent)).mkString("\n", "\n", "\n") + indent.toSpace + "</html>" // Represent <html> tag def html(init: Html ?=> Unit): Html = given h: Html = Html() init h
  34. Context Function: Html テンプレート • Div: <div> を表すクラス ◦ toHtmlString

    メソッドでは、子タグにコンテキストとしてインデントを渡している ◦ div メソッドでは、子タグを生成するブロック init にコンテキスト Div を渡している /** Model for <div> tag */ class Div extends Tag: def toHtmlString = (indent: Indent) ?=> val childIndent = indent + 1 indent.toSpace + "<div>" + childs.map(child => child.toHtmlString(using childIndent)).mkString("\n", "\n", "\n") + indent.toSpace + "</div>" /** Represent <div> tag */ def div(init: Div ?=> Unit)(using t: Tag): Unit = given d: Div = Div() init t.add(d)
  35. Context Function: Html テンプレート • P: <p> を表すクラス ◦ p

    タグに子タグは入らないので、ただコンテキストを受け取るだけ /** Model for <p> tag */ class P(body: String) extends Tag: def toHtmlString = (indent: Indent) ?=> s"${indent.toSpace}<p>${body}</p>" /** Represent <p> tag */ def p(body: String)(using t: Tag): Unit = t.add(P(body))
  36. Context Function: Html テンプレート • Img: <img> を表すクラス ◦ img

    タグに子タグは入らないので、ただコンテキストを受け取るだけ ◦ 簡略化のため src 属性だけ実装 /** Model for <img> tag */ class Img(src: String) extends Tag: def toHtmlString = (indent: Indent) ?=> s"${indent.toSpace}<img src=\"${src}\" />" /** Represent <img> tag */ def img(src: String)(using t: Tag): Unit = t.add(Img(src))
  37. Context Function: Html テンプレート • 実行してみよう @main def runHtml(): Unit

    = given Indent = Indent(0) val template = html { div { p("Hello") img("/assets/images/hoge.jpg") } } println(template.toHtmlString) sbt:have-fun-scala3> chapters/runMain chapter3.runHtml [info] running chapter3.runHtml <html> <div> <p>Hello</p> <img src="/assets/images/hoge.jpg" /> </div> </html>
  38. Context Function: なにが嬉しいか • 制約チェックにも使える object PostConditionExample: opaque type WrappedResult[T]

    = T def result[T](using r: WrappedResult[T]): T = r extension [T](x: T) def ensuring(condition: WrappedResult[T] ?=> Boolean): T = assert(condition(using x)) x @main def runPostCondition(): Unit = /** Should be 6 */ println(Try(List(1, 2, 3).sum.ensuring(result == 6))) /** Should be error */ println(Try(List(1, 2).sum.ensuring(result == 6)))
  39. 環境構築 • 以下のリポジトリをクローン ◦ pushする場合はフォークしましょう ◦ have-fun-scala3 • sbt 起動

    • テスト実行 ◦ 全部エラーになる ◦ テストを全部通していこう # フォークしたリポジトリをクローン $ git clone [email protected]:yourname/have-fun-scala3.git # sbt 起動 $ sbt # プロジェクトを exercise に変更 sbt:have-fun-scala3> project exercise [info] set current project to exercise (in build file:/path/to/have-fun-scala3/) sbt:exercise> # テスト実行 sbt:exercise> testOnly exercise.chapter3.*
  40. 練習問題1: Given インスタンス trait Ord[T]: def compare(x: T, y: T):

    Int 順序に関する性質を定義した Ord という型クラスに対して、インスタンス Ord[Int] と Ord[Option[T]] を Question1 オブジェクト内に定義してください。ただし、 compare メソッドは以下の仕様とします。 • x が y より小さいなら -1 を返す • x が y より大きいなら 1 を返す • x と y が等しければ 0 を返す • Ord[Option[T]] に関して ◦ None は Some(_) よりも小さい ◦ Some(a) と Some(b) の大小は、a と b の大小に等しい https://github.com/taretmch/have-fun-scala3/blob/master/exercise/src/main/scala/exercise/chapter3/Question1.scala
  41. Ord を型パラメータに持つリスト List[T: Ord] に対して、リスト内の最大の値を返 すメソッド max を Question2 オブジェクト内に定義してください。

    ただし、空リストに対しては例外を吐いていいものとします。 https://github.com/taretmch/have-fun-scala3/blob/master/exercise/src/main/scala/exercise/chapter3/Question2.scala 練習問題2: Using def max[T](list: List[T]): T = ...
  42. 以下の拡張メソッドを Question3 オブジェクト内に定義してください。 • 型 T: Ord の値 x が

    T の値 y より小さいかどうかを返すメソッド ◦ x > y: Boolean • 型 T: Ord の値 x が T の値 y より大きいかどうかを返すメソッド ◦ x < y: Boolean • 型 T: Ord のリスト list: List[T] の最大値を返すメソッド ◦ list.max: T ◦ ※ Question2 のメソッドを使ってもよい https://github.com/taretmch/have-fun-scala3/blob/master/exercise/src/main/scala/exercise/chapter3/Question3.scala 練習問題3: 拡張メソッド
  43. [1] New in Scala 3, https://docs.scala-lang.org/scala3/new-in-scala3.html [2] Given Instances, https://docs.scala-lang.org/scala3/reference/contextual/givens.html

    [3] Import Givens, https://docs.scala-lang.org/scala3/reference/contextual/given-imports.html [4] Using Clauses, https://docs.scala-lang.org/scala3/reference/contextual/using-clauses.html [5] Predef, https://scala-lang.org/api/3.x/scala/Predef$.html#summon-957 [6] Extension Methods, https://docs.scala-lang.org/scala3/reference/contextual/extension-methods.html [7] Implicit Conversions, https://docs.scala-lang.org/scala3/reference/contextual/conversions.html [8] dotty/Conversion, https://github.com/lampepfl/dotty/blob/3.0.2/library/src/scala/Conversion.scala#L25 [9] Context Functions, https://docs.scala-lang.org/scala3/reference/contextual/context-functions.html 参考リンク