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

Essential Scala 第5章 シーケンス処理 / Essential Scala Chapter 5 Sequencing Computations

Essential Scala 第5章 シーケンス処理 / Essential Scala Chapter 5 Sequencing Computations

Takuya Tsuchida

September 12, 2018
Tweet

More Decks by Takuya Tsuchida

Other Decks in Programming

Transcript

  1. 5.1.1 パンドラの箱 リストよりシンプルなコレクション、単一の値を保持する Box から始めます。Box にどん な型が保持されるかは気にしたくないですが、Box から値を取り出したとき、その型を維 持してほしいです。これを総称型を使用して実現します。 final

    case class Box[A](value: A) Box(2) // res0: Box[Int] = Box(2) res0.value // res1: Int = 2 Box("hi") // 型引数を省略すると Scala は値から型を推論する // res2: Box[String] = Box(hi) res2.value // res3: String = hi
  2. 5.1.1 パンドラの箱 型引数はメソッド引数と類似した動作をします。メソッドを呼ぶときにメソッド引数名に値 を束縛します。generic(1) と呼ぶとき、引数 in は generic の本体で1という値に束縛さ れます。

    型引数を伴うメソッドやコンストラクターを呼ぶとき、型引数はメソッドやクラス本体で具 象型に束縛します。generic(1) と呼ぶとき、型引数 A は generic の本体で Int に束縛さ れます。
  3. 5.1.2 総称代数的データ型 結果が Double に限定されず、総称型になるよう、これを総称化してみます。また、数値 計算に制限されないよう Calculation を Result という名前にします。

    型 A の Result は型 A の Success か String の文字列を伴う Failure です。 sealed trait Result[A] case class Success[A](result: A) extends Result[A] case class Failure[A](reason: String) extends Result[A] Success と Failure のどちらも、Result を拡張するところで渡される型引数 A を導入し ていることに気付きます。Success は型 A の値を持っていますが、Failure は型 A を導 入しているだけです。後節で変位を導入するときに、この実装について明快な方針を示 します。
  4. ノート:非変総称直和型パターン 型 T の A が B か C である場合、このように記述する。

    sealed trait A[T] final case class B[T]() extends A[T] final case class C[T]() extends A[T] 17
  5. sealed trait IntList { def length: Int = this match

    { case End => 0 case Pair(hd, tl) => 1 + tl.length } def double: IntList = this match { case End => End case Pair(hd, tl) => Pair(hd * 2, tl.double) } def product: Int = this match { case End => 1 case Pair(hd, tl) => hd * tl.product } def sum: Int = this match { case End => 0 case Pair(hd, tl) => hd + tl.sum } } final case object End extends IntList final case class Pair(head: Int, tail: IntList) extends IntList
  6. 5.2 関数 本稿の序盤で、文法の仕組みによってオブジェクトを関数として取り扱える apply メソッ ドを紹介しました。 object add1 { def

    apply(in: Int) = in + 1 } add1(2) // res: Int = 3 これは Scala で本物の関数プログラミングを実現するための大きな進歩ですが、型とい う大事なコンポーネントのひとつを見逃しています。Scala の関数型を見ていきましょう。
  7. 5.2.1 関数型 関数型は (A, B) => C というように書け、A と B

    は引数型を、C は結果型を表現しま す。同じパターンで無引数や有引数の関数を一般化します。 2つの Int を引数として Int を返す関数 f としたいならば、(Int, Int) => Int と記述します。
  8. ノート:関数型宣言文法 関数型は下記のように宣言する。 (A, B, ...) => C • A, B,

    ... は引数型 • C は結果型 関数がひとつの引数だけである場合は、括弧を省略できる。 A => B 27
  9. 5.2.2 関数リテラル Scala は新しい関数を生成する関数リテラル文法を与えてくれます。 val sayHi = () => "Hi!"

    // sayHi: () => String = <function0> sayHi() // res: String = Hi! val add1 = (x: Int) => x + 1 // add1: Int => Int = <function1> add1(10) // res: Int = 11 val sum = (x: Int, y: Int) => x + y // sum: (Int, Int) => Int = <function2> sum(10, 20) // res: Int = 30
  10. 5.3.1 畳み込み LinkedList[A] を拡張するのはかなり素直な対応です。単に Pair の先頭要素を Int では なく型 A

    にするだけです。 sealed trait LinkedList[A] { def fold[B](end: B, f: (A, B) => B): B = this match { case End() => end case Pair(hd, tl) => f(hd, tl.fold(end, f)) } } final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A] final case class End[A]() extends LinkedList[A]
  11. ノート:畳み込みパターン 代数的データ型 A について、畳み込みはそれを総称型 B に変換する。畳み込みは下記 を伴う構造的再帰である。 • A の各ケースについてひとつの関数引数

    • 各関数はその関連するクラスのフィールドを引数としてとる • A が再帰的な場合、再帰的フィールドを参照するどの関数引数も型 B の引数をとる マッチするケースのパターンの右辺側、ないしは適切な多相メソッドは、適切な関数呼び 出しで構成される。 38
  12. 5.3.1 畳み込み パターンを適用して fold メソッドを引き出してみましょう。基本的なテンプレートからス タートしてみます。 sealed trait LinkedList[A] {

    def fold[B](???): B = this match { case End() => ??? case Pair(hd, tl) => ??? } } final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A] final case class End[A]() extends LinkedList[A] これは結果型として総称型引数を追加した構造的再帰のテンプレートです。
  13. 5.3.1 畳み込み 関数型についての規則から次のように決定できます。 End は値を保持しないので end は無引数で B を返します。よってその型は ()

    => B で、単に型 B の値として最適化できます。 Pair は2引数を持ち、ひとつはリストの先頭で、もうひとつはリストの末尾です。head に ついての引数は型 A で、tail についての引数は再帰のなので型 B です。よって最終的 に型は (A, B) => B です。 def fold[B](end: B, pair: (A, B) => B): B = this match { case End() => end case Pair(hd, tl) => pair(hd, tl.fold(end, pair)) }
  14. 5.3.2.1 プレイスホルダー文法 とても単純な状況ではプレイスホルダー文法と呼ばれる簡略文法を使用することでイン ライン関数を書けます。 ((_: Int) * 2) // res:

    Int => Int = <function1> (_: Int) * 2 はコンパイラーによって (a: Int) => a * 2 に展開されます。慣用的には、コン パイラーが型を推論できる場合にのみ、プレイスホルダー文法を使用します。
  15. 5.3.2.1 プレイスホルダー文法 さらにいくつかの例を見てみましょう。 _ + _ // (a, b) =>

    a + b foo(_) // (a) => foo(a) foo(_, b) // (a) => foo(a, b) _(foo) // (a) => a(foo) プレイスホルダー文法は、大きな式において使用すると理解しづらくなるため、とても小 さな関数においてのみ使用すべきです。
  16. 5.3.3 メソッドから関数への変換 * Scala はメソッドコールを関数に変換する機能を含みます。この機能はプレイスホル ダー文法と関連しており、アンダースコアをメソッドの末尾に付与します。 object Sum { def

    sum(x: Int, y: Int) = x + y } Sum.sum // <console>:9: error: missing arguments for method sum in object Sum; // follow this method with `_' if you want to treat it as a ... // Sum.sum // ^ (Sum.sum _) // res: (Int, Int) => Int = <function2> * 訳注:節番号が 5.3.2.2 になる内容です。おそらく誤植と考えられます。
  17. 5.3.3.1 複数引数列 複数引数列は2つの利用目的があります。1つ目は、コードブロックのように関数を記述 する機能のためです。 def fold[B](end: B)(pair: (A, B) =>

    B): B = this match { case End() => end case Pair(hd, tl) => pair(hd, tl.fold(end, pair)) } これによって下記のように呼び出すことができます。 fold(0){ (total, elt) => total + elt } 下記よりは読み易くなります。 fold(0, (total, elt) => total + elt)
  18. 5.3.3.1 複数引数列 2つ目は、型推論を容易にするためです。Scala の型推論アルゴリズムは、ある引数の 型推論結果をほかの引数に利用できません。 下記のような宣言では、Scala が end について B

    を推論しても、pair で B の型推論に利用できないので、pair に型宣言を書く必要があります。 def fold[B](end: B, pair: (A, B) => B): B しかし、Scala はある引数列での型推論を、ほかの引数列での型推論に利用できます。 そのため、下記のように記述することで、end の B についての型推論を pair 型の推論 に利用できます。 def fold[B](end: B)(pair: (A, B) => B): B
  19. 5.4.1 総称直積型 Pair などが直積型を総称を使用して生成するもので、両方の返却型についての関連す るデータを含みます。 def intAndString: Pair[Int, String] =

    // … def booleanAndDouble: Pair[Boolean, Double] = // … 総称は、直積型を定義するための異なる手法を提供し、継承と反対で集約に依拠してい ます。
  20. 5.4.2 タプル タプルはペアの一般化です。Scala には22要素までの組み込みの総称タプル型があ り、それらを生成するための特別な文法があります。 そのクラスは Tuple1[A] から Tupple22[A, B,

    C, ...] ですが、糖衣構文で (A, B, C, ...) と記述することができます。 Tuple2("hi", 1) // res: (String, Int) = (hi,1) ("hi", 1) // res: (String, Int) = (hi,1) ("hi", 1, true) // res: (String, Int, Boolean) = (hi,1,true)
  21. 5.4.4 総称任意値 式は値を生成することもあれば、生成しないこともあります。例えば、キーによってハッ シュテーブルの要素を参照するとき、値が存在しないことがあります。Web サービスと 通信している場合、サービスが停止していて返信がないかもしれません。また、ファイル を参照する場合、そのファイルは削除されてしまっているかもしれません。任意値を使用 してこのような状況をモデリングする方法がいくつかあります。値が存在しないときに、 例外を投げたり、null を返したりします。この2つの方法が持つ欠点は型システムについ

    てのどんな情報も含まないということです。 一般に堅牢なプログラムを書きたいもので、Scala では型システムを活用して、プログラ ムを保守するための特質を表現します。ある共通の特質は「エラーを正しく処理する」と いうものです。型システムで任意値を表現する場合、コンパイラーは値が存在しない場 合を考えることを強制するので、コードの堅牢性が増大します。
  22. 5.5.1 マップ (Map) 下記の例は、共通に型 F[A] と関数 A => B を持ち、結果

    F[B] を得ます。この処理を実 行するメソッドはマップと呼ばれます。 • 「ユーザー ID のリスト」「ユーザー ID からユーザーレコードを取得する関数」を持 つ。ID のリストからレコードのリストを取得したい。型として書くと、List[Int] と関数 Int => User を持ち、List[User] を取得したい。 • 「データベースから読み込まれたユーザーレコードを表現する任意値」「注文を読み 込む関数」を持つ。レコードがある場合、注文を取得したい。Maybe[User] と関数 User => Order を持ち、Maybe[Order] を取得したい。 • 「エラーメッセージか注文を表現する直和型」を持つ。注文がある場合、注文の合計 値を取得したい。Sum[String, Order] と関数 Order => Double を持ち、 Sum[String, Double] を取得したい。
  23. 5.5.1 マップ (Map) LinkedList におけるマップを実装してみます。型と一般的な構造的再帰のスケルトンを 与えるところから始めます。 sealed trait LinkedList[A] {

    def map[B](fn: A => B): LinkedList[B] = this match { case Pair(hd, tl) => ??? case End() => ??? } } final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A] final case class End[A]() extends LinkedList[A]
  24. 5.5.1 マップ (Map) Pair は、先頭と末尾を組み合わせて LinkedList[B] を返します。また、末尾は再帰する 必要があります。 case Pair(hd,

    tl) => { val newTail: LinkedList[B] = tl.map(fn) // Combine newTail and head to create LinkedList[B] } fn 関数を使用して先頭を B に変換し、末尾を再帰したリストから大きなリストを構築しま す。 case Pair(hd, tl) => Pair(fn(hd), tl.map(fn))
  25. 5.5.1 マップ (Map) End は、関数を適用できる A の値を持ちません。End を返すだけです。 sealed trait

    LinkedList[A] { def map[B](fn: A => B): LinkedList[B] = this match { case Pair(hd, tl) => Pair(fn(hd), tl.map(fn)) case End() => End[B]() } } 型とパターンが解決に導いてくれていることに気付きますよね。
  26. 5.5.2 フラットマップ (FlatMap) 下記の例を想像してください。 • 「ユーザーのリスト」を持ち、すべてのユーザーの注文のリストを取得したい。型とし て書くと、LinkedList[User] と関数 User =>

    LinkedList[Order] を持ち、 LinkedList[Order] を取得したい。 • 「データベースから読み込まれたユーザーレコードを表現する任意値」を持ち、別の 任意値として最新の注文を取得したい。型として書くと、Maybe[User] と関数 User => Maybe[Order] を持ち、Maybe[Order] を取得したい。 • 「エラーメッセージか注文を表現する直和型」を持ち、ユーザーに請求書をメール送 信したい。メールはエラーメッセージかメッセージ ID を返す。型として書くと、 Sum[String, Order] と関数 Order => Sum[String, Id] を持ち、Sum[String, Id] を 取得したい。
  27. 5.5.2 フラットマップ (FlatMap) すべての例は共通に型 F[A] と関数 A => F[B] を持ち、結果

    F[B] を取得したいことにな ります。この処理を実行するメソッドを flatMap と呼びます。 Maybe について flatMap を実装してみましょう。まずは型を下書きするところから始め ます。 sealed trait Maybe[A] { def flatMap[B](fn: A => Maybe[B]): Maybe[B] = ??? } final case class Full[A](value: A) extends Maybe[A] final case class Empty[A]() extends Maybe[A]
  28. 5.5.2 フラットマップ (FlatMap) メソッド本体を埋めるために、前と同じパターン、構造的再帰と型の導きを使用します。 sealed trait Maybe[A] { def flatMap[B](fn:

    A => Maybe[B]): Maybe[B] = this match { case Full(v) => fn(v) case Empty() => Empty[B]() } } final case class Full[A](value: A) extends Maybe[A] final case class Empty[A]() extends Maybe[A]
  29. 5.5.3 関手 (Functor) とモナド (Monad) map メソッドを持つ F[A] のような型を関手と呼びます。さらに flatMap

    メソッドをも持つ 場合はモナドと呼びます。 関手やモナドであるためにはもう少し必要なものがあります。モナドについては、point と 呼ばれるコンストラクターと、map や flatMap が必ず従わなければならない代数的規則 が必要です。モナドについての情報はオンラインで見付けることもできますし、 Advanced Scala でより詳細について解説しています。
  30. 5.5.3 関手 (Functor) とモナド (Monad) モナドはある文脈における値を表現します。文脈は使用しているモナドに依存します。 • 任意値。例えば、データベースから取得する値を表現する • 直和値。例えば、エラーメッセージか計算値を表現する

    • 値のリスト 同じ文脈を維持したまま、文脈に含まれる値を新しい値に変換するとき map を使用しま す。値を変換し新しい文脈を与えるとき flatMap を使用します。
  31. 5.6 変位 本節では、型の間における派生クラス関係の制御を型引数によって可能にする変位注 釈について説明します。非変総称直和型パターンを改めて見てみましょう。 sealed trait Maybe[A] final case class

    Full[A](value: A) extends Maybe[A] final case class Empty[A]() extends Maybe[A] 理想的には Empty の使用されていない型引数を下記のように取り除きたいです。 sealed trait Maybe[A] final case class Full[A](value: A) extends Maybe[A] final case object Empty extends Maybe[???]
  32. 5.6 変位 オブジェクトは型引数を持ちません。Empty オブジェクトを作成するためには その定義 より Maybe を拡張した具象型を与える必要があります。しかし、どの型引数を使用すべ きでしょうか?特定のデータ型が与えられない場合、Unit や

    Nothing のようなものを使 用しますが、これは型エラーを引き起こします。 val possible: Maybe[Int] = Empty // <console>:9: error: type mismatch; // found : Empty.type // required: Maybe[Int] // val possible: Maybe[Int] = Empty Empty は Maybe[Nothing] であり、Maybe[Nothing] は Maybe[Int] の派生型ではない ことが問題です。この問題を克服するために変位注釈を使用します。
  33. 5.6.1 非変・共変・反変 型 Foo[A] を持ち、A は B の派生型である場合、Foo[A] は Foo[B]

    の派生型でしょう か?この答えは型 Foo の変位に依存します。総称型の変位は、その型引数による基底 型と派生型の関係を決定します。 型 Foo[T] は T の観点で非変で、A と B の関係に関わらず、Foo[A] と Foo[B] は無関 係です。Scala のどんな総称型においてもデフォルトの変位です。 型 Foo[+T] は T の観点で共変で、A が B の派生型である場合、Foo[A] は Foo[B] の 派生型です。ほとんどの Scala コレクションクラスはそれらの内容の観点で共変です。 型 Foo[-T] は T の観点で反変で、A が B の派生型である場合、Foo[A] は Foo[B] の 基底型です。反変の唯一の例は関数引数です。
  34. 5.6.2 関数型 変位を理解するには、どんな関数を map メソッドに安全に渡せるか検討します。 • A から B への関数は明らかに

    OK である。 • A から B の派生型への関数は OK である。なぜなら、その結果型は B の属性す べてを持っているためだ。これは、結果型において共変である関数を示している。 • A の基底型から B への関数も OK である。なぜなら、Box が持つ A は関数が期 待する属性すべてを持っているためだ。 • A の派生型から B への関数は OK ではない。なぜなら、値は A の派生型とはおそ らく異なるためだ。
  35. 5.6.3 共変直和型 変位注釈について理解したので、Maybe 問題を共変にすることで解決できます。 sealed trait Maybe[+A] final case class

    Full[A](value: A) extends Maybe[A] final case object Empty extends Maybe[Nothing] 使用してみると期待する動作を得られます。Empty はすべての Full 値の派生型になり ます。 val perhaps: Maybe[Int] = Empty // perhaps: Maybe[Int] = Empty このパターンは総称直和型で頻繁に使用されます。共変型は、コンテナー型が不変であ るときのみ使用すべきです。コンテナーが可変である場合、非変型のみ使用すべきで す。
  36. ノート:共変総称直和型パターン 型 T の A が B か C であり、C

    が総称でない場合、このように記述する。 sealed trait A[+T] final case class B[T](t: T) extends A[T] final case object C extends A[Nothing] このパターンはひとつ以上の型引数を拡張する。直和型の特定ケースで型引数が必要 とされない場合、その型引数を Nothing で置換できる。 105
  37. 5.6.4 反変ポジション 共変直和型について学ぶ必要があるほかのパターンとして、共変型引数の相互作用 と、反変なメソッドと関数引数があります。共変の Sum を実装することでその問題を明 らかにしましょう。 sealed trait Sum[+A,

    +B] { def flatMap[C](f: B => Sum[A, C]): Sum[A, C] = this match { case Failure(v) => Failure(v) case Success(v) => f(v) } } final case class Failure[A](value: A) extends Sum[A, Nothing] final case class Success[B](value: B) extends Sum[Nothing, B]
  38. 5.6.4 反変ポジション この問題をより単純な例で考えてみましょう。 case class Box[+A](value: A) { def set(a:

    A): Box[A] = Box(a) } // <console>:12: error: covariant type A occurs in ... // def set(a: A): Box[A] = Box(a) // ^
  39. 5.6.4 反変ポジション flatMap に戻ると、関数 f は引数なので反変ポジションです。f の基底型を受け入れられ るということになります。型 B =>

    Sum[A, C] と宣言でき、基底型は B について共変で、 A と C について反変です。B は共変として宣言されているので問題ありません。C は非 変なので問題ありません。一方、反変ポジションにおいて A は共変になっています。 よって Box で使用した解法を適用します。 sealed trait Sum[+A, +B] { def flatMap[AA >: A, C](f: B => Sum[AA, C]): Sum[AA, C] = this match { case Failure(v) => Failure(v) case Success(v) => f(v) } } final case class Failure[A](value: A) extends Sum[A, Nothing] final case class Success[B](value: B) extends Sum[Nothing, B]
  40. ノート:反変ポジションパターン 共変型 T の A があり、A のメソッド f において T

    が反変ポジションで使用されていると 警告される場合、f において型 TT >: T を導入する。 case class A[+T] { def f[TT >: T](t: TT): A[TT] } 112
  41. 5.6.5 型境界 反変ポジションパターンにおいて型境界を見てきました。型境界は指定の派生型や基底 型を拡張します。A <: Type は A が Type

    の派生型でなければならないことを宣言し、 A >: Type は A が Type の基底型でなければならないことを宣言する文法です。 下記の例は Visitor かその派生型を保持することを可能にします。 case class WebAnalytics[A <: Visitor]( visitor: A, pageViews: Int, searchTerms: List[String], isOrganic: Boolean )