Step 3: 変位の実践
3-1. 共変の実践 ― すでに使っている
共変を見るのにカスタム型は要らない ― 毎日使う標準ライブラリの型に入っている。
どれも 2-5 で見た [B >: A] 下限境界の脱出口を示している:
// step2h.scala — Covariance in the standard library
trait Item:
def name: String
def price: Double
class Book(val name: String, val price: Double, val isbn: String) extends Item:
override def toString = s"Book($name)"
class DVD(val name: String, val price: Double) extends Item:
override def toString = s"DVD($name)"
@main def step2h(): Unit =
val book = Book("Scala in Depth", 45.0, "978-1617295")
val dvd = DVD("The Matrix", 19.99)
// --- Option[+A] ---
// Some[Book] is usable as Option[Item] — a Book result IS an Item result
val maybeBook: Option[Book] = Some(book)
val maybeItem: Option[Item] = maybeBook // covariance
println(maybeItem) // Some(Book(Scala in Depth))
// getOrElse uses [B >: A] — the lower bound escape hatch for covariance
val item: Item = maybeBook.getOrElse(dvd) // Option[Book] → fallback DVD → Item
println(item)
// --- List[+A] ---
// List[Book] is usable as List[Item]
val books: List[Book] = List(book)
val items: List[Item] = books // covariance
println(items)
// :+ uses [B >: A] — adding a DVD widens to List[Item]
val mixed: List[Item] = books :+ dvd
println(mixed) // List(Book(Scala in Depth), DVD(The Matrix))
// --- Nothing ---
// Nothing is Scala's bottom type — a subtype of every other type.
// No value of type Nothing can ever exist, but the compiler uses it as a
// placeholder: "this side has no type yet." Combined with covariance,
// Nothing <: anything, so it fits wherever a type is expected.
// --- Either[+A, +B] ---
// Either is covariant in BOTH type parameters
// Right(book) is Right[Nothing, Book] — Nothing is the Left type.
// Covariance on +A: Nothing <: String, so Either[Nothing, Book] <: Either[String, Book]
val result: Either[String, Book] = Right(book)
val wider: Either[String, Item] = result // covariance on +B: Book <: Item
println(wider)
// Same idea with Left: Left("not found") is Left[String, Nothing]
// Covariance on +B: Nothing <: Book, so Either[String, Nothing] <: Either[String, Book]
val failed: Either[String, Book] = Left("not found")
println(failed)
// orElse uses [B1 >: B] — recovery widens the Right type
val recovered: Either[String, Item] = failed.orElse(Right(dvd))
println(recovered) // Right(DVD(The Matrix))
このコードは Nothing ― Scala のボトム型を紹介する。すべての型のサブタイプであり、
Nothing 型の値は決して存在できない。
コンパイラは型の片側が未指定のときにプレースホルダーとして使う:
Right(book)はRight[Nothing, Book]― Left 型はまだないLeft("error")はLeft[String, Nothing]― Right 型はまだないList()はList[Nothing]― 空、要素型はまだないNoneはOption[Nothing]― 値がない、型もまだない
共変がこれを機能させる。任意の A に対して Nothing <: A なので、
型が期待される場所にいつでも適合する。2-5 ですでに見た:Cart() は
Cart[Nothing] だ。Book を追加すると [B >: A] によって Cart[Book] に拡大する。
パターンはすべて同じだ:型は A の値を生産(返す、保持する、発行する)するので、
+A が自然だ。異なるサブタイプの値を追加・結合する必要があるときは、
[B >: A] が安全に型を拡大する。
3-2. 反変の実践 ― ハンドラ、バリデータ、シリアライザ
反変はより稀だが、非常に特定のパターンで現れる: 値を消費または処理する型だ。
// step2i.scala
trait Item:
def name: String
def price: Double
class Book(val name: String, val price: Double, val isbn: String) extends Item:
override def toString = s"Book($name, $price, $isbn)"
class DVD(val name: String, val price: Double) extends Item:
override def toString = s"DVD($name, $price)"
// JSON serializer — consumes a value and produces a String
trait JsonWriter[-A]:
def write(value: A): String
class ItemWriter extends JsonWriter[Item]:
def write(value: Item): String =
s"""{"name":"${value.name}","price":${value.price}}"""
class BookWriter extends JsonWriter[Book]:
def write(value: Book): String =
s"""{"name":"${value.name}","price":${value.price},""" +
s""""isbn":"${value.isbn}"}"""
// Validator — consumes a value and returns pass/fail
trait Validator[-A]:
def validate(value: A): Boolean
class PriceValidator extends Validator[Item]:
def validate(value: Item): Boolean = value.price > 0
def serialize[A](value: A, writer: JsonWriter[A]): String =
writer.write(value)
@main def step2i(): Unit =
val itemWriter = ItemWriter()
val book = Book("Scala in Depth", 45.0, "978-1617295")
// serialize expects JsonWriter[Book] — ItemWriter handles any Item, so it qualifies
println(serialize(book, itemWriter))
// {"name":"Scala in Depth","price":45.0} — no isbn, but it works
// A BookWriter knows about Book-specific fields
val bookWriter = BookWriter()
println(serialize(book, bookWriter))
// {"name":"Scala in Depth","price":45.0,"isbn":"978-1617295"}
// The reverse doesn't work: a BookWriter can't handle arbitrary Items
// serialize(DVD("The Matrix", 19.99), bookWriter) // Compile error
// What would bookWriter do with a DVD's isbn?
// Same pattern with Validator: PriceValidator validates any Item,
// so it works on Books too
val books = List(Book("Scala", 45.0, "978-1"), Book("Free", 0.0, "978-2"))
val valid = books.filter(PriceValidator().validate)
println(valid) // List(Book(Scala, 45.0, 978-1))
2-7 ですでにこのパターンを見た:Function1[-A, +B] は入力に対して反変であり、
cheap が Item => Boolean のとき books.filter(cheap) が動作する理由だ。
ここの JsonWriter は同じアイデアだ。serialize は JsonWriter[Book] を期待し、
ItemWriter(JsonWriter[Item])が適格だ ― 任意の Item をシリアライズできるなら、
Book もシリアライズできる。Validator も同じパターンだ:
任意の Item を検証する PriceValidator は books.filter でも機能する。
3-3. では不変は本当に役立つのか?
役立つ。ただし役割は思うより狭い。
不変は型が読み書き両方するときに正しい選択だ ― 可変コンテナ、 双方向チャネル、読み書き参照。しかし良く設計された Scala コードでは、 不変性が好まれるため比較的稀だ。
// step2j.scala
trait Item:
def name: String
def price: Double
class Book(val name: String, val price: Double, val isbn: String) extends Item:
override def toString = s"Book($name)"
// Invariant is correct here: a mutable cell reads AND writes
class Cell[A](var value: A):
def get: A = value // output position (+)
def set(a: A): Unit = // input position (-)
value = a
// A bidirectional codec: encodes AND decodes
trait Codec[A]:
def encode(value: A): String // A in input position (-)
def decode(raw: String): A // A in output position (+)
// Both positions → must be invariant. The compiler enforces this.
// Try adding + or - and see what happens:
// trait Codec[+A]: // error on encode
// trait Codec[-A]: // error on decode
@main def step2j(): Unit =
val cell = Cell[Item](Book("Scala", 45.0, "978-1"))
cell.set(Book("FP", 35.0, "978-2"))
println(cell.get) // Book(FP)
// Cell[Book] is NOT Cell[Item] — and shouldn't be.
// If it were, you could put a DVD in a Book cell.
実際のところ:
Scala で設計する型のほとんどは二つの陣営のどちらかに入る:
A を生産する(読み取り専用データ、結果、イベント、ストリーム) → 共変にする [+A]
A を消費する(ハンドラ、ライター、バリデータ、順序付け) → 反変にする [-A]
不変はアノテーションしないときに得られるものだ ― そしてそれは多くの場合、
自分の型がプロデューサーなのかコンシューマーなのかまだ考えていないからだ。
考えてみると、+ か - のどちらかが大抵当てはまることに気づくだろう。
「この型は A を生産するのか消費するのか?」と問う規律自体が
貴重な設計エクササイズだ。型の役割を明確にすることを強制し、
コンパイラがその答えを検証してくれる。