15. 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 を導
入しているだけです。後節で変位を導入するときに、この実装について明快な方針を示
します。
16.
17. ノート:非変総称直和型パターン
型 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
19. 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
26. 5.2.1 関数型
関数型は (A, B) => C というように書け、A と B は引数型を、C は結果型を表現しま
す。同じパターンで無引数や有引数の関数を一般化します。
2つの Int を引数として Int を返す関数 f としたいならば、(Int, Int) => Int と記述します。
35. 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]
38. ノート:畳み込みパターン
代数的データ型 A について、畳み込みはそれを総称型 B に変換する。畳み込みは下記
を伴う構造的再帰である。
● A の各ケースについてひとつの関数引数
● 各関数はその関連するクラスのフィールドを引数としてとる
● A が再帰的な場合、再帰的フィールドを参照するどの関数引数も型 B の引数をとる
マッチするケースのパターンの右辺側、ないしは適切な多相メソッドは、適切な関数呼び
出しで構成される。
38
39. 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]
これは結果型として総称型引数を追加した構造的再帰のテンプレートです。
41. 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))
}
45. 5.3.2.1 プレイスホルダー文法
さらにいくつかの例を見てみましょう。
_ + _ // (a, b) => a + b
foo(_) // (a) => foo(a)
foo(_, b) // (a) => foo(a, b)
_(foo) // (a) => a(foo)
プレイスホルダー文法は、大きな式において使用すると理解しづらくなるため、とても小
さな関数においてのみ使用すべきです。
46. 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 になる内容です。おそらく誤植と考えられます。
75. 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] を取得したい。
76. 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]
79. 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))
80. 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]()
}
}
型とパターンが解決に導いてくれていることに気付きますよね。
81.
82. 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] を
取得したい。
83. 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]
84.
85. 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]
102. 5.6.2 関数型
変位を理解するには、どんな関数を map メソッドに安全に渡せるか検討します。
● A から B への関数は明らかに OK である。
● A から B の派生型への関数は OK である。なぜなら、その結果型は B の属性す
べてを持っているためだ。これは、結果型において共変である関数を示している。
● A の基底型から B への関数も OK である。なぜなら、Box が持つ A は関数が期
待する属性すべてを持っているためだ。
● A の派生型から B への関数は OK ではない。なぜなら、値は A の派生型とはおそ
らく異なるためだ。
103. 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
このパターンは総称直和型で頻繁に使用されます。共変型は、コンテナー型が不変であ
るときのみ使用すべきです。コンテナーが可変である場合、非変型のみ使用すべきで
す。
104.
105. ノート:共変総称直和型パターン
型 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
106. 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]
107.
108. 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)
// ^
111. 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]
112. ノート:反変ポジションパターン
共変型 T の A があり、A のメソッド f において T が反変ポジションで使用されていると
警告される場合、f において型 TT >: T を導入する。
case class A[+T] {
def f[TT >: T](t: TT): A[TT]
}
112
113. 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
)