Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Essential Scala 第4章 トレイトによるデータモデリング / Essential Scala Chapter 4 Modelling Data with Traits

Essential Scala 第4章 トレイトによるデータモデリング / Essential Scala Chapter 4 Modelling Data with Traits

Takuya Tsuchida

August 01, 2018
Tweet

More Decks by Takuya Tsuchida

Other Decks in Programming

Transcript

  1. 第4章 トレイトによるデータモデリング 前章でクラスの奥深さを見ました。クラスは類似した属性を持つオブジェクトを抽象化す る方法を提供し、クラスに属するどんなオブジェクトでも動作するコードを書けるようにな りました。 本章ではクラスを抽象化する方法を探検し、異なるクラスのオブジェクトでも動作する コードを書きます。これをトレイトと呼ばれるメカニズムで実現します。 本章は視点の変更を示します。前章では Scala コードを構成する技術的側面を重視し

    てきました。本章のはじめでもトレイトの技術的側面を重視します。その視点は、思考を 表現する媒体として Scala を使用することに変化していきます。 代数的データ型と呼ばれるデータの記述をコードに機械的に変換する方法を見ていきま す。構造的再帰を使用することで、代数的データ型を変換するコードを機械的に記述で きます。 3
  2. 4.1.1 トレイトの例 case class Anonymous( id: String, createdAt: Date =

    new Date() ) extends Visitor case class User( id: String, email: String, createdAt: Date = new Date() ) extends Visitor
  3. 4.1.1 トレイトの例 下記の2つについて変更しています。 • Visitor トレイトを定義する • extends キーワードを使用し Visitor

    トレイトの派生型として Anonymous と User を宣言する Visitor トレイトは、どんな派生型でも必ず実装しなければならないインターフェイスを表 現します。派生型は id と呼ばれる文字列と日付の createdAt を必ず実装します。また、 どんな Visitor の派生型でも Visitor に定義されている age メソッドを自動的に持ちま す。
  4. 4.1.1 トレイトの例 Visitor トレイトを定義することによって、どんな訪問者の派生型についても動作するメ ソッドを書くことができます。 def older(v1: Visitor, v2: Visitor):

    Boolean = v1.createdAt.before(v2.createdAt) older(Anonymous("1"), User("2", "[email protected]")) // res4: Boolean = true older メソッドは Visitor の派生型である Anonymous でも User でも呼び出すことがで きます。
  5. 4.1.2 トレイトとクラスの比較 抽象的な定義を探すために Visitor トレイトに立ち返ってみましょう。 trait Visitor { def id:

    String // 各ユーザーに付与されるユニークな ID def createdAt: Date // このユーザーがサイトにはじめて訪れた日付 // 訪問者がどのくらいの時間そこにいたか? def age: Long = new Date().getTime - createdAt.getTime } Visitor は2つの抽象メソッドを規定しています。メソッドは実装を持ちませんが、派生クラ スで実装されなければなりません。その対象として id と createdAt があります。また、ひ とつの抽象メソッドを項として定義に含む、具象メソッドである age を定義しています。
  6. 4.1.2 トレイトとクラスの比較 Visitor は Anonymous と User という2つのクラスのためのビルディングブロックとして 使用されています。各クラスは Visitor

    を拡張しており、それはそのフィールドとメソッド のすべてを継承していることを意味します。 Anonymous("anon1") // res14: Anonymous = Anonymous(anon1) res14.createdAt // res15: java.util.Date = Mon Mar 24 15:11:45 GMT 2014 res14.age // res16: Long = 8871
  7. 4.1.2 トレイトとクラスの比較 id と createdAt は抽象なので、それらは派生クラスで定義される必要があります。例の クラスでは def ではなく val

    として実装しています。Scala ではこれが認められており、 val を一般化したものが def であると見なされます * 。トレイトの中で val を定義せずに def を使用することはいいことです。具体的な実装は適切に def か val を使用して実装 しましょう。 * オブジェクトリテラルについての課題で見た統一形式アクセスの原則のことです。
  8. 4.2 あれかこれかで他にはない:シールドトレイト シールドトレイトはトレイト宣言に sealed と記述するだけで作成できます。 sealed trait Visitor { def

    id: String def createdAt: Date def age: Long = new Date().getTime() - createdAt.getTime() } case class User(id: String, email: String, createdAt: Date = new Date()) extends Visitor case class Anonymous(id: String, createdAt: Date = new Date()) extends Visitor
  9. ノート:シールドトレイト文法 トレイトにおけるすべての派生型が既知であればトレイトを封印すべきである。 sealed trait TraitName { ... } 派生型を拡張することがなければ、派生型を final

    にすることを検討すべきである。 final case class Name(...) extends TraitName { ... } なお、派生型はシールドトレイトと同じファイルで定義されなければならない。 26
  10. 4.2.1 キーポイント:シールドトレイト シールドトレイトとファイナル(ケース)クラスは型の拡張性を制御できるようにします。大 多数はシールドトレイトとファイナルケースクラスのパターンとして使用されます。 sealed trait TraitName { ... }

    final case class Name(...) extends TraitName このパターンの主な利点は、コンパイラーがパターンマッチで不足している型を警告して くれることと、シールドトレイトを拡張する場所を制御することで派生型の振る舞いを保証 できることです。
  11. 4.3 トレイトによるデータモデリング 本節では言語機能からプログラミングパターンに焦点を移していきます。データモデリン グについて見ていきながら、論理和と論理積の見地から様々なデータモデルをScala で 表現するプロセスを学びます。オブジェクト指向プログラミングの専門用語である is-a 関 係と has-a

    関係を表現し、関数プログラミングの専門用語である、代数的データ型と呼 ばれる直和型と直積型を学びます。 本節でのゴールは、どうデータモデルを Scala コードに変換するかを見るということで す。次節では代数的データ型を使用したコードのパターンを見ます。
  12. ノート:直積型パターン A が B 型の b と C 型の c

    を持つ場合、このように記述する。 case class A(b: B, c: C) or trait A { def b: B def c: C } 30
  13. 4.4.2 不足しているパターン is-a / has-a と and / or という2軸の関係性を見てきました。この定義を表にすると4つの

    セルについて2つのパターンしか見ていません。 2つの不足しているパターンは何でしょうか? And Or Is-a 直和型 Has-a 直積型
  14. 4.4.2 不足しているパターン is-a and パターンは「A は B かつ C である」を意味します。このパターンはある点にお

    いて直和型の逆になり、下記のように実装できます。 trait B trait C trait A extends B with C
  15. 4.4.2 不足しているパターン has-a or パターンは「A は B か C を持つ」を意味します。これを実装するには2つの方

    法があります。 「A は D を持ち、D は B か C である」と言えます。これを実装するために2つのパターン を機械的に適用することができます。 trait A { def d: D } sealed trait D final case class B() extends D final case class C() extends D
  16. 4.4.2 不足しているパターン あるいは、「A は D か E である、D は B

    を持ち、E は C を持つ」と言えます。これも直 接的にコードに変換できます。 sealed trait A final case class D(b: B) extends A final case class E(c: C) extends A
  17. 4.4.3 キーポイント:トレイトによるデータモデリング 直積型による has-a and パターンと直和型による is-a or パターンで、データを機械的 に

    Scala コードを変換するのを見ました。このタイプのデータは代数的データ型として知 られています。それらのパターンを理解することは理想的な Scala コードを書く上でとて も重要です。
  18. 4.5.1 ポリモーフィズムを使用した構造的再帰 直和型を使用したシンプルな定義から始めます。 sealed trait A { def foo: String

    } final case class B() extends A { def foo: String = "It's B!" } final case class C() extends A { def foo: String = "It's C!" }
  19. 4.5.1 ポリモーフィズムを使用した構造的再帰 val anA: A = B() // anA: A

    = B() anA.foo // res1: String = It's B! val anA: A = C() // anA: A = C() anA.foo // res2: String = It's C!
  20. 4.5.1 ポリモーフィズムを使用した構造的再帰 トレイトで実装を定義することもでき、派生クラスでは override キーワードを使用して実 装を変更します。 sealed trait A {

    def foo: String = "It's A!" } final case class B() extends A { override def foo: String = "It's B!" } final case class C() extends A { override def foo: String = "It's C!" }
  21. 4.5.1 ポリモーフィズムを使用した構造的再帰 val anA: A = B() // anA: A

    = B() anA.foo // res1: String = It's B! トレイトでデフォルト実装を提供する場合、すべての派生型で妥当な実装であることを確 実にすべきということを覚えておいてください。
  22. ノート:直積型ポリモーフィズムパターン A が B 型の b と C 型の c

    を持ち、F 型を返すメソッド f を記述したい場合、シンプルに いつもの方法でメソッドを記述する。 case class A(b: B, c: C) { def f: F = ??? } メソッドの本体で、F 型の結果を構築するために、 b と c とメソッド引数を必ず使用しな ければならない。 48
  23. ノート:直和型ポリモーフィズムパターン A が B か C であり、F 型を返すメソッド f を記述したい場合、A

    で f を抽象メソッドとして 定義し、B と C で具象実装を提供する。 sealed trait A { def f: F } final case class B() extends A { def f: F = ??? } final case class C() extends A { def f: F = ??? } 49
  24. ノート:直積型パターンマッチパターン A が B 型の b と C 型の c

    を持ち、A を受け入れ F を返すメソッド f を記述したい場合、 下記のように記述する。 def f(a: A): F = a match { case A(b, c) => ??? } メソッドの本体で、F 型の結果を構築するために、 b と c を使用する。 51
  25. ノート:直和型パターンマッチパターン A が B か C であり、A を受け入れ F を返すメソッド

    f を記述したい場合、B と C をパ ターンマッチのケースとして定義する。 def f(a: A): F = a match { case B() => ??? case C() => ??? } 52
  26. 4.5.3 完全な例:共通 sealed trait Food final case object Antelope extends

    Food final case object TigerFood extends Food final case object Licorice extends Food final case class CatFood(food: String) extends Food
  27. 4.5.3 完全な例:ポリモーフィズム sealed trait Feline { def dinner: Food }

    final case class Lion() extends Feline { def dinner: Food = Antelope } final case class Tiger() extends Feline { def dinner: Food = TigerFood } final case class Panther() extends Feline { def dinner: Food = Licorice } final case class Cat(favouriteFood: String) extends Feline { def dinner: Food = CatFood(favouriteFood) }
  28. 4.5.3 完全な例:パターンマッチ(基底トレイト) sealed trait Feline { def dinner: Food =

    this match { case Lion() => Antelope case Tiger() => TigerFood case Panther() => Licorice case Cat(favouriteFood) => CatFood(favouriteFood) } } final case class Lion() extends Feline final case class Tiger() extends Feline final case class Panther() extends Feline final case class Cat(favouriteFood: String) extends Feline
  29. 4.5.3 完全な例:パターンマッチ(別オブジェクト) sealed trait Feline final case class Lion() extends

    Feline final case class Tiger() extends Feline final case class Panther() extends Feline final case class Cat(favouriteFood: String) extends Feline object Diner { def dinner(feline: Feline): Food = feline match { case Lion() => Antelope case Tiger() => TigerFood case Panther() => Licorice case Cat(food) => CatFood(food) } }
  30. 4.5.4 使用するパターンの選び方 構造的再帰を実装するのに3つの方法があります。 1. ポリモーフィズム 2. 基底トレイトでのパターンマッチ 3. 外部オブジェクトでのパターンマッチ 使用するパターンは下記のルールを参考に選んでみてください。

    • 実装がクラス内のフィールドやメソッドだけに依存している → 1 or 2(一般的に重複するコードが少ない2が選ばれる) • 実装がクラス外のデータに依存している → 3 • 複数の実装が必要になる → 3
  31. 4.6 再帰的データ とくに代数的データ型は再帰的データの定義によく使用されます。このデータはそれ自 身の項によって定義され、サイズを制約しないデータを作成します。 ただし、再帰が永遠に続くため、このようには定義できません。 final case class Broken(broken: Broken)

    最後を表すベースケースを定義することで妥当な再帰的データを定義できます。 sealed trait IntList final case object End extends IntList final case class Pair(head: Int, tail: IntList) extends IntList Pair(1, Pair(2, Pair(3, End))) // res: Pair = Pair(1,Pair(2,Pair(3,End)))
  32. 4.6 再帰的データ End の解答は 0 と決定できます。Pair は Int 型を返す必要があり、tl について再帰呼

    び出しをする必要があります。 def sum(list: IntList): Int = list match { case End => 0 case Pair(hd, tl) => ??? sum(tl) }
  33. 4.6.2 末尾再帰 import scala.annotation.tailrec @tailrec def sum(list: IntList): Int =

    list match { case End => 0 case Pair(hd, tl) => hd + sum(tl) } // <console>:18: error: could not optimize @tailrec annotated method sum: it ... // def sum(list: IntList): Int = list match { // ^ @tailrec def sum(list: IntList, total: Int = 0): Int = list match { case End => total case Pair(hd, tl) => sum(tl, total + hd) } // sum: (list: IntList, total: Int)Int
  34. 4.7 大きな例 * 大きなプロジェクトの例です。 1. Calculator 2. JSON 3. Music

    * 訳注:課題の節なので説明は割愛します。
  35. 4.8 まとめ 本章では重要な視点の変更を行い、言語機能から離れ、言語機能がサポートするプロ グラミングパターンに目を向けました。これは本稿の残りでも継続します。 2つの重要なパターン、代数的データ型と構造的再帰を探究しました。それらのパターン は、データのメンタルモデルから、まったく機械的な方法による Scala におけるデータの 表現と処理に我々を向かわせます。月並みなコードの構造や理解を容易にするだけで はなく、コンパイラーが開発や保守を容易にするために共通のエラーを捕捉してくれま

    す。これら2つのツールは、慣用的な関数的コードで一般に使用され、その重要性はど れほど強調してもしすぎることはありません。 本章における課題の中で共通のデータ構造を開発しましたが、固定された型のデータし か保持できなく、そのコードは多くの繰り返しが含まれていました。次章では、型とメソッ ドを超えた抽象化を見ていきながら、シーケンス処理におけるいくつかの重要なコンセプ トを紹介します。