Successfully reported this slideshow.
We use your LinkedIn profile and activity data to personalize ads and to show you more relevant ads. You can change your ad preferences anytime.

Essential Scala 第5章 シーケンス処理

11 views

Published on

2018年8月23日と2018年9月12日にアカウンティング・サース・ジャパン株式会社で開催した「Essential Scala 輪読会 #4」「Essential Scala 輪読会 #5」のスライドです。今回の内容は「第5章 シーケンス処理」です。

Published in: Technology
  • Be the first to comment

  • Be the first to like this

Essential Scala 第5章 シーケンス処理

  1. 1. Essential Scala 第5章 シーケンス処理 2018.9.12 Takuya Tsuchida (@takuya0301)
  2. 2. 第5章 シーケンス処理 5.1 総称 5.2 関数 5.3 総称データのための総称畳み込み 5.4 総称型によるデータモデリング 5.5 シーケンス処理 5.6 変位 5.7 まとめ 2
  3. 3. 第5章 シーケンス処理 本章ではさらに2つの言語機能である総称と関数を見ていきます。そして、それらの機能 を使用して構築する抽象化の関手とモナドも見ていきます。 前章で開発したコードから始めます。整数のリストである IntList を開発し、課題で下記 のようなコードを書きました。 sealed trait IntList { 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
  4. 4. 第5章 シーケンス処理 このコードには2つの問題があります。1つ目はリストが Int に強く制限されていることで す。2つ目は多くの繰り返しがあることです。このコードは共通の構造を持っているので、 構造的再帰パターンを適用することに違和感はなく、多くの重複を削減できるでしょう。 本章ではこれら両方の問題に注力します。前半で型を抽象化する総称を使用し、ユー ザーが指定した型で動作するデータを作成します。後半でメソッドを抽象化する関数を 使用し、コードの重複を削減します。 それらのテクニックを適用することで、いくつかの一般的なパターンが出現します。本章 の最後で、それらのパターンに名前を付け、詳細に研究してみることにしましょう。
  5. 5. 5.1 総称 総称型は型を抽象化することを可能にします。すべてのデータ構造において有用です が、一般にコレクションで遭遇するので、そこから始めていきましょう。
  6. 6. 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
  7. 7. 5.1.1 パンドラの箱 [A] という文法は型引数と呼ばれます。型引数はメソッドにも付与でき、メソッドの宣言と 本体にその引数のスコープを限定します。 def generic[A](in: A): A = in generic[String]("foo") // res: String = foo generic(1) // 型引数を省略すると Scala は値から型を推論する // res: Int = 1
  8. 8. 5.1.1 パンドラの箱 型引数はメソッド引数と類似した動作をします。メソッドを呼ぶときにメソッド引数名に値 を束縛します。generic(1) と呼ぶとき、引数 in は generic の本体で1という値に束縛さ れます。 型引数を伴うメソッドやコンストラクターを呼ぶとき、型引数はメソッドやクラス本体で具 象型に束縛します。generic(1) と呼ぶとき、型引数 A は generic の本体で Int に束縛さ れます。
  9. 9. ノート:型パラメーター文法 総称型は [A, B, C] のようにブラケットのリストで宣言する。慣例として総称型には1文字 の大文字を使用する。 総称型はクラス宣言やトレイト宣言において宣言され、その宣言における残りの部分で 登場できる。 case class Name[A](...){ ... } trait Name[A]{ ... } あるいは、メソッド宣言で宣言され、メソッド内で登場できる。 def name[A](...){ ... } 12
  10. 10. 5.1.2 総称代数的データ型 メソッド引数に類似する型引数を説明し、型引数を持つトレイトを拡張するときも類似し ています。直和型でしたように、トレイトを拡張することは、メソッド呼び出しについて型の レベルで等価で、拡張したトレイトのどんな型引数についても値を供給しなければなりま せん。 前章で下記のような直和型を見ました。 sealed trait Calculation final case class Success(result: Double) extends Calculation final case class Failure(reason: String) extends Calculation
  11. 11. 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 を導 入しているだけです。後節で変位を導入するときに、この実装について明快な方針を示 します。
  12. 12. ノート:非変総称直和型パターン 型 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
  13. 13. 5.2 関数 関数はメソッドを抽象化することを可能にし、プログラムの中で渡したり操作したりできる ようにメソッドを値に変化させます。 前述の IntList を操作するメソッドを見てみましょう。
  14. 14. 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
  15. 15. 5.2 関数 メソッドのすべてが共通のパターンを持つことは、それらすべてが構造的再帰を使用し ていることからも驚きはありません。その重複を取り除くことができたら素敵です。 Int を返すメソッド、length、product、sumにフォーカスしてみると、そのメソッドはこのよ うに書けます。 def abstraction(end: Int, f: ???): Int = this match { case End => end case Pair(hd, tl) => f(hd, tl.abstraction(end, f)) }
  16. 16. 5.2 関数 Pair ケースにおいて、先頭と再帰呼び出しの組み合わせを処理するあるオブジェクトを 記述するのに f を使用しました。この値の型を書き下す方法も知りませんし、それを構 築する方法も知りません。しかし、この節のタイトルから恐らく推測できるでしょう。ここで 必要なのは関数であるということを! 関数はメソッドのようなもので、引数と共に呼び出すことができ、結果に評価されます。メ ソッドと違って関数は値です。関数はメソッドやほかの関数に渡すことができます。また、 メソッドから関数を返すことも可能です。
  17. 17. 5.2 関数 本稿の序盤で、文法の仕組みによってオブジェクトを関数として取り扱える apply メソッ ドを紹介しました。 object add1 { def apply(in: Int) = in + 1 } add1(2) // res: Int = 3 これは Scala で本物の関数プログラミングを実現するための大きな進歩ですが、型とい う大事なコンポーネントのひとつを見逃しています。Scala の関数型を見ていきましょう。
  18. 18. 5.2.1 関数型 関数型は (A, B) => C というように書け、A と B は引数型を、C は結果型を表現しま す。同じパターンで無引数や有引数の関数を一般化します。 2つの Int を引数として Int を返す関数 f としたいならば、(Int, Int) => Int と記述します。
  19. 19. ノート:関数型宣言文法 関数型は下記のように宣言する。 (A, B, ...) => C ● A, B, ... は引数型 ● C は結果型 関数がひとつの引数だけである場合は、括弧を省略できる。 A => B 27
  20. 20. 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
  21. 21. 5.2.2 関数リテラル 引数型が既知であるコードにおいて、型注釈を省略することができ、Scala が推論してく れます。関数の結果型を宣言する文法はなく、通常それは推論されますが、関数本体 の式に型を指定することでも実現できます。 (x: Int) => (x + 1): Int
  22. 22. ノート:関数リテラル文法 関数リテラルは下記のように宣言する。 (parameter: type, ...) => expression ● parameter は関数引数の名前(省略可能) ● type は関数引数の型 ● expression は関数の結果を決定する 31
  23. 23. 5.3 総称データのための総称畳み込み 総称データのクラスを定義するのを見てきましたが、クラス上にたくさんのメソッドを実装 はしませんでした。ユーザーは総称型を供給し、さらにユーザーにその型で機能する関 数を供給することを要請します。それでもやはり、そこには総称データを使用するいくつ かの共通パターンがあり、それをこの節で探索していきます。IntList の文脈で畳み込み はすでに見ています。ここで畳み込み (fold) の詳細を探索していき、様々な代数的デー タ型について畳み込みを実装するパターンについて学びます。
  24. 24. 5.3.1 畳み込み (Fold) 整数型のリストを処理する畳み込みを見ました。総称型のリストに一般化してみましょ う。必要な道具はすでに見てきています。最初にデータ定義を不変直和型パターンを使 用して少し修正します。 sealed trait LinkedList[A] final case class Pair[A](head: A, tail: LinkedList[A]) extends LinkedList[A] final case class End[A]() extends LinkedList[A]
  25. 25. 5.3.1 畳み込み IntList で見た畳み込みの最終バージョンは下記のとおりです。 def fold[A](end: A, f: (Int, A) => A): A = this match { case End => end case Pair(hd, tl) => f(hd, tl.fold(end, f)) }
  26. 26. 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]
  27. 27. 5.3.1 畳み込み 畳み込みはちょうど構造的再帰の適応で、ユーザーが関数にそれぞれの場合に適用す るものを渡すことができます。構造的再帰としての代数的データ型を変換する関数を書 くための総称的パターンで、畳み込みはこの総称的パターンの具体的な実装です。畳み 込みは総称的変換と反復処理です。どんな関数も代数的データ型について書くときは畳 み込みという手段で書けるか気にかけるべきです。
  28. 28. ノート:畳み込みパターン 代数的データ型 A について、畳み込みはそれを総称型 B に変換する。畳み込みは下記 を伴う構造的再帰である。 ● A の各ケースについてひとつの関数引数 ● 各関数はその関連するクラスのフィールドを引数としてとる ● A が再帰的な場合、再帰的フィールドを参照するどの関数引数も型 B の引数をとる マッチするケースのパターンの右辺側、ないしは適切な多相メソッドは、適切な関数呼び 出しで構成される。 38
  29. 29. 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] これは結果型として総称型引数を追加した構造的再帰のテンプレートです。
  30. 30. 5.3.1 畳み込み LinkedList の2つのクラスそれぞれに対応する関数を追加します。 def fold[B](end: ???, pair: ???): B = this match { case End() => ??? case Pair(hd, tl) => ??? }
  31. 31. 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)) }
  32. 32. 5.3.2 関数で作業する Scala には、高階関数として知られる、関数を受け入れる関数やメソッドで作業するため の芸当があります。ここから見ていくのは下記のものです。 1. 関数を書くための簡潔な文法 2. メソッドから関数への変換 3. 型推論を活用した高階メソッドの書き方
  33. 33. 5.3.2.1 プレイスホルダー文法 とても単純な状況ではプレイスホルダー文法と呼ばれる簡略文法を使用することでイン ライン関数を書けます。 ((_: Int) * 2) // res: Int => Int = <function1> (_: Int) * 2 はコンパイラーによって (a: Int) => a * 2 に展開されます。慣用的には、コン パイラーが型を推論できる場合にのみ、プレイスホルダー文法を使用します。
  34. 34. 5.3.2.1 プレイスホルダー文法 さらにいくつかの例を見てみましょう。 _ + _ // (a, b) => a + b foo(_) // (a) => foo(a) foo(_, b) // (a) => foo(a, b) _(foo) // (a) => a(foo) プレイスホルダー文法は、大きな式において使用すると理解しづらくなるため、とても小 さな関数においてのみ使用すべきです。
  35. 35. 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 になる内容です。おそらく誤植と考えられます。
  36. 36. 5.3.3 メソッドから関数への変換 Scala は関数が必要なところと推論し、アンダースコアを省略してメソッド名だけを記述 できます。コンパイラーは自動的にメソッドを関数に昇格させます。 object MathStuff { def add1(num: Int) = num + 1 } Counter(2).adjust(MathStuff.add1) // res: Counter = Counter(3)
  37. 37. 5.3.3.1 複数引数列 * Scala のメソッドは複数の引数列を持てます。それらのメソッドは、各引数列を括弧で囲 む必要があることを除いて、普通のメソッドのように動作します。 def example(x: Int)(y: Int) = x + y // example: (x: Int)(y: Int)Int example(1)(2) // res: Int = 3 * 訳注:節番号が 5.3.2.3 になる内容です。おそらく誤植と考えられます。
  38. 38. 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)
  39. 39. 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
  40. 40. 5.4 総称型によるデータモデリング 本節では、データモデリングのときに利用できる総称型の追加機能を見ていきます。総 称直和型と総称直積型として実装される総称型と、任意値のように便利な抽象のモデ ルをいくつか見ます。
  41. 41. 5.4.1 総称直積型 直積型をモデリングするために総称を使用してみましょう。2値を返すメソッドについて考 えてみます。 def intAndString: ??? = // ... def booleanAndDouble: ??? = // ...
  42. 42. 5.4.1 総称直積型 問題は返却型として何を使うべきかということです。型引数を持たない代数的データ型 パターンのクラスを使用できますが、返却型の組み合わせごとにひとつのクラスを実装 しなければなりません。 case class IntAndString(intValue: Int, stringValue: String) def intAndString: IntAndString = // ... case class BooleanAndDouble(booleanValue: Boolean, doubleValue: Double) def booleanAndDouble: BooleanAndDouble = // ...
  43. 43. 5.4.1 総称直積型 Pair などが直積型を総称を使用して生成するもので、両方の返却型についての関連す るデータを含みます。 def intAndString: Pair[Int, String] = // … def booleanAndDouble: Pair[Boolean, Double] = // … 総称は、直積型を定義するための異なる手法を提供し、継承と反対で集約に依拠してい ます。
  44. 44. 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)
  45. 45. 5.4.2 タプル 引数としてタプルを受け取るメソッドを同じ文法を使用して定義できます。 def tuplized[A, B](in: (A, B)) = in._1 // tuplized: [A, B](in: (A, B))A tuplized(("a", 1)) // res: String = a
  46. 46. 5.4.2 タプル 下記のようにタプルでパターンマッチすることもできます。 (1, "a") match { case (a, b) => a + b } // res: String = 1a
  47. 47. 5.4.2 タプル また、パターンマッチはタプルを分解する自然な方法で、各クラスは _1 や _2 などと名 付けられたフィールドを持ちます。 val x = (1, "b", true) // x: (Int, String, Boolean) = (1,b,true) x._1 // res: Int = 1 x._3 // res: Boolean = true
  48. 48. 5.4.3 総称直和型 直和型をモデリングするために総称を使用してみましょう。まえに、代数的データ型パ ターンを使用して実装し、共通属性を基底型に含めました。このパターンと異なる抽象を 総称は実現し、一般化した実装を提供します。 そのメソッドについて考えると、その引数の値に依存し、2つの型のうち1つを返します。 def intOrString(input: Boolean) = if (input == true) 123 else "abc" // intOrString: (input: Boolean)Any
  49. 49. 5.4.3 総称直和型 前述のメソッドは、コンパイラーが Any として結果型を推論してしまうので、安易に書く わけにはいきません。その代わり、選言を明示的に表現するために新しい型を導入しま す。 def intOrString(input: Boolean): Sum[Int, String] = if(input == true) { Left[Int, String](123) } else { Right[Int, String]("abc") }
  50. 50. 5.4.4 総称任意値 式は値を生成することもあれば、生成しないこともあります。例えば、キーによってハッ シュテーブルの要素を参照するとき、値が存在しないことがあります。Web サービスと 通信している場合、サービスが停止していて返信がないかもしれません。また、ファイル を参照する場合、そのファイルは削除されてしまっているかもしれません。任意値を使用 してこのような状況をモデリングする方法がいくつかあります。値が存在しないときに、 例外を投げたり、null を返したりします。この2つの方法が持つ欠点は型システムについ てのどんな情報も含まないということです。 一般に堅牢なプログラムを書きたいもので、Scala では型システムを活用して、プログラ ムを保守するための特質を表現します。ある共通の特質は「エラーを正しく処理する」と いうものです。型システムで任意値を表現する場合、コンパイラーは値が存在しない場 合を考えることを強制するので、コードの堅牢性が増大します。
  51. 51. 5.4.5 キーポイント:総称型によるデータモデリング 本節では直積型と直和型をモデリングするために総称を使用しました。 それらの抽象は Scala コードで普通に使用され、Scala の標準ライブラリの実装に含ま れます。直積型はタプルで、直和型は Either で、任意値は Option でモデリングされま す。
  52. 52. 5.5 シーケンス処理 総称データと代数的データ型の畳み込みを修得しました。さて、処理におけるいくつか他 の共通パターンを見ていきましょう。1つ目、場合によっては代数的データ型について畳 み込みするよりも便利なものです。2つ目、畳み込みできないあるデータのタイプについ て実装を可能にするものです。それらのメソッドは Map と FlatMap として知られていま す。
  53. 53. 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] を取得したい。
  54. 54. 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]
  55. 55. 5.5.1 マップ (Map) 代数的データ型における普遍的なイテレーターである畳み込みを構造的再帰パターンと して使用します。
  56. 56. 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))
  57. 57. 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]() } } 型とパターンが解決に導いてくれていることに気付きますよね。
  58. 58. 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] を 取得したい。
  59. 59. 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]
  60. 60. 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]
  61. 61. 5.5.3 関手 (Functor) とモナド (Monad) map メソッドを持つ F[A] のような型を関手と呼びます。さらに flatMap メソッドをも持つ 場合はモナドと呼びます。 関手やモナドであるためにはもう少し必要なものがあります。モナドについては、point と 呼ばれるコンストラクターと、map や flatMap が必ず従わなければならない代数的規則 が必要です。モナドについての情報はオンラインで見付けることもできますし、 Advanced Scala でより詳細について解説しています。
  62. 62. 5.5.3 関手 (Functor) とモナド (Monad) もっとも、map や flatMap の目下における応用はリストのようなコレクションクラスで、大 局的な見地としてはシーケンス処理です。失敗を含む処理の並びを想像してみてくださ い。 def mightFail1: Maybe[Int] = Full(1) def mightFail2: Maybe[Int] = Full(2) def mightFail3: Maybe[Int] = Empty[Int] // これが失敗になる
  63. 63. 5.5.3 関手 (Functor) とモナド (Monad) 別の処理の後にある処理を実行したいです。それらのうちひとつでも失敗した場合、す べての処理は失敗します。そうでない場合、すべての数を足し上げたものを取得しま す。これを flatMap で実現できます。 下記の結果は Empty になります。 mightFail1 flatMap { x => mightFail2 flatMap { y => mightFail3 flatMap { z => Full(x + y + z) } } }
  64. 64. 5.5.3 関手 (Functor) とモナド (Monad) mightFail3 を取り除いた下記の結果は Full(3) になります。 mightFail1 flatMap { x => mightFail2 flatMap { y => Full(x + y) } }
  65. 65. 5.5.3 関手 (Functor) とモナド (Monad) モナドはある文脈における値を表現します。文脈は使用しているモナドに依存します。 ● 任意値。例えば、データベースから取得する値を表現する ● 直和値。例えば、エラーメッセージか計算値を表現する ● 値のリスト 同じ文脈を維持したまま、文脈に含まれる値を新しい値に変換するとき map を使用しま す。値を変換し新しい文脈を与えるとき flatMap を使用します。
  66. 66. 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[???]
  67. 67. 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] の派生型ではない ことが問題です。この問題を克服するために変位注釈を使用します。
  68. 68. ノート:変位は難しい 変位は Scala 型システムの巧妙な側面のひとつです。アプリケーションコードで使用する ことがめったにないとしても、それが存在していることを認識していることは有用です。 98
  69. 69. 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] の 基底型です。反変の唯一の例は関数引数です。
  70. 70. 5.6.2 関数型 関数型について話したときその具体的な実装に立ち入りませんでした。Scala は、0引数 から22引数までの関数について、23のビルトイン総称クラスを持ちます。 trait Function0[+R] { def apply: R } trait Function1[-A, +B] { def apply(a: A): B } trait Function2[-A, -B, +C] { def apply(a: A, b: B): C } // and so on...
  71. 71. 5.6.2 関数型 関数は、それらの引数の観点で反変で、それらの返却型の観点で共変です。これは直 感に反しますが、関数引数の視点から見ることで理解できるはずです。Function1[A, B] を期待するコードについて検討してみましょう。 class Box[A](value: A) { /** value に func を適用し、結果の Box を返す。 */ def map[B](func: Function1[A, B]): Box[B] = Box(func(a)) }
  72. 72. 5.6.2 関数型 変位を理解するには、どんな関数を map メソッドに安全に渡せるか検討します。 ● A から B への関数は明らかに OK である。 ● A から B の派生型への関数は OK である。なぜなら、その結果型は B の属性す べてを持っているためだ。これは、結果型において共変である関数を示している。 ● A の基底型から B への関数も OK である。なぜなら、Box が持つ A は関数が期 待する属性すべてを持っているためだ。 ● A の派生型から B への関数は OK ではない。なぜなら、値は A の派生型とはおそ らく異なるためだ。
  73. 73. 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 このパターンは総称直和型で頻繁に使用されます。共変型は、コンテナー型が不変であ るときのみ使用すべきです。コンテナーが可変である場合、非変型のみ使用すべきで す。
  74. 74. ノート:共変総称直和型パターン 型 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
  75. 75. 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]
  76. 76. 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) // ^
  77. 77. 5.6.4 反変ポジション 関数やメソッドにおける引数は反変であることを思い出してください。このケースでは、A は共変であると指定していますが、型 A の引数を持つので、型規則は A が反変である ことを求めています。このためコンパイラーが「反変ポジション」と伝えているのです。 A の基底型である新しい型を導入することが解決になります。これを [AA >: A] という記 法で実現できます。 case class Box[+A](value: A) { def set[AA >: A](a: AA): Box[AA] = Box(a) }
  78. 78. 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]
  79. 79. ノート:反変ポジションパターン 共変型 T の A があり、A のメソッド f において T が反変ポジションで使用されていると 警告される場合、f において型 TT >: T を導入する。 case class A[+T] { def f[TT >: T](t: TT): A[TT] } 112
  80. 80. 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 )
  81. 81. 5.7 まとめ 本章では、型とメソッドを抽象化する総称型と関数を探究しました。 総称代数的データ型における新しいパターンと総称構造的再帰を見ました。それらのビ ルディングブロックを使用することで、総称型を扱うためのいくつかの共通パターン、 fold・map・flatMap を見ました。 次章では、さらに Scala のコレクションクラスで作業する話題を探究します。

×