第3章: 多態性とディスパッチ¶
はじめに¶
多態性(ポリモーフィズム)は、同じインターフェースで異なる振る舞いを実現する強力な概念です。Scala では、sealed trait、トレイト、型クラス、パターンマッチングという複数のメカニズムで多態性を実現します。
本章では、これらのメカニズムを使い分けて、柔軟で拡張性の高いコードを書く方法を学びます。
1. Sealed Trait による多態性(代数的データ型)¶
Scala の sealed trait は、有限の型のバリエーションを定義するのに最適です。コンパイラがパターンマッチングの網羅性をチェックしてくれます。
基本的な使い方¶
// 図形を表す sealed trait
sealed trait Shape
case class Rectangle(width: Double, height: Double) extends Shape
case class Circle(radius: Double) extends Shape
case class Triangle(base: Double, height: Double) extends Shape
// 図形の面積を計算する
def calculateArea(shape: Shape): Double = shape match
case Rectangle(w, h) => w * h
case Circle(r) => Math.PI * r * r
case Triangle(b, h) => b * h / 2
// 使用例
calculateArea(Rectangle(4, 5)) // => 20.0
calculateArea(Circle(3)) // => 28.27...
calculateArea(Triangle(6, 5)) // => 15.0
Sealed Trait の利点¶
- 網羅性チェック: コンパイラがすべてのケースがカバーされているか確認
- 型安全性: 不正な型の値を渡せない
- パターンマッチング: 分岐を簡潔に記述可能
2. 複合ディスパッチ¶
タプルを使ったパターンマッチングで、複数の値に基づくディスパッチを実現できます。
enum PaymentMethod:
case CreditCard, BankTransfer, Cash
enum Currency:
case JPY, USD, EUR
case class Payment(method: PaymentMethod, currency: Currency, amount: Int)
case class PaymentResult(status: String, message: String, amount: Int, converted: Option[Int] = None)
def processPayment(payment: Payment): PaymentResult =
import PaymentMethod.*, Currency.*
(payment.method, payment.currency) match
case (CreditCard, JPY) =>
PaymentResult("processed", "クレジットカード(円)で処理しました", payment.amount)
case (CreditCard, USD) =>
PaymentResult("processed", "Credit card (USD) processed", payment.amount, Some(payment.amount * 150))
case (BankTransfer, JPY) =>
PaymentResult("pending", "銀行振込を受け付けました", payment.amount)
case _ =>
PaymentResult("error", "サポートされていない支払い方法です", payment.amount)
// 使用例
processPayment(Payment(PaymentMethod.CreditCard, Currency.JPY, 1000))
// => PaymentResult("processed", "クレジットカード(円)で処理しました", 1000)
3. 階層的ディスパッチ(継承による)¶
Scala では継承を使って型の階層を定義し、各型に固有の振る舞いを持たせることができます。
sealed trait Account:
def balance: Int
def interestRate: Double
case class SavingsAccount(balance: Int) extends Account:
val interestRate = 0.02
case class PremiumSavingsAccount(balance: Int) extends Account:
val interestRate = 0.05
case class CheckingAccount(balance: Int) extends Account:
val interestRate = 0.001
def calculateInterest(account: Account): Double =
account.balance * account.interestRate
// 使用例
calculateInterest(SavingsAccount(10000)) // => 200.0
calculateInterest(PremiumSavingsAccount(10000)) // => 500.0
calculateInterest(CheckingAccount(10000)) // => 10.0
4. トレイト(Protocols に相当)¶
トレイトは、特定の操作セットを定義するインターフェースです。Java のインターフェースに似ていますが、デフォルト実装を持てます。
トレイトの定義¶
trait Drawable:
def draw: String
def boundingBox: BoundingBox
case class BoundingBox(x: Double, y: Double, width: Double, height: Double)
trait Transformable[T]:
def translate(dx: Double, dy: Double): T
def scale(factor: Double): T
def rotate(angle: Double): T
トレイトの利点¶
- パフォーマンス: パターンマッチングより高速な型ベースのディスパッチ
- 明確なコントラクト: 実装すべきメソッドが明示的
- ミックスイン: 複数のトレイトを組み合わせ可能
5. トレイトを実装するケースクラス¶
case class DrawableRectangle(x: Double, y: Double, width: Double, height: Double)
extends Drawable with Transformable[DrawableRectangle]:
def draw: String = s"Rectangle at ($x,$y) with size ${width}x$height"
def boundingBox: BoundingBox = BoundingBox(x, y, width, height)
def translate(dx: Double, dy: Double): DrawableRectangle =
copy(x = x + dx, y = y + dy)
def scale(factor: Double): DrawableRectangle =
copy(width = width * factor, height = height * factor)
def rotate(angle: Double): DrawableRectangle = this
case class DrawableCircle(x: Double, y: Double, radius: Double)
extends Drawable with Transformable[DrawableCircle]:
def draw: String = s"Circle at ($x,$y) with radius $radius"
def boundingBox: BoundingBox =
BoundingBox(x - radius, y - radius, radius * 2, radius * 2)
def translate(dx: Double, dy: Double): DrawableCircle =
copy(x = x + dx, y = y + dy)
def scale(factor: Double): DrawableCircle =
copy(radius = radius * factor)
def rotate(angle: Double): DrawableCircle = this
// 使用例
val rect = DrawableRectangle(10, 20, 100, 50)
rect.draw // => "Rectangle at (10.0,20.0) with size 100.0x50.0"
rect.translate(5, 10) // => DrawableRectangle(15.0, 30.0, 100.0, 50.0)
val circle = DrawableCircle(50, 50, 25)
circle.boundingBox // => BoundingBox(25.0, 25.0, 50.0, 50.0)
6. 型クラス(既存型への拡張)¶
型クラスは、既存の型に後から振る舞いを追加できる強力なパターンです。Clojure の extend-protocol に相当します。
// 型クラスの定義
trait Stringable[A]:
def stringify(a: A): String
object Stringable:
def apply[A](using s: Stringable[A]): Stringable[A] = s
given Stringable[Map[String, Any]] with
def stringify(m: Map[String, Any]): String =
"{" + m.map((k, v) => s"$k: $v").mkString(", ") + "}"
given Stringable[List[Any]] with
def stringify(l: List[Any]): String =
"[" + l.mkString(", ") + "]"
given Stringable[String] with
def stringify(s: String): String = s
given Stringable[Int] with
def stringify(i: Int): String = i.toString
given optionStringable[A](using s: Stringable[A]): Stringable[Option[A]] with
def stringify(opt: Option[A]): String = opt match
case Some(a) => s.stringify(a)
case None => "nil"
// 拡張メソッド
extension [A](a: A)
def toCustomString(using s: Stringable[A]): String = s.stringify(a)
// 使用例
val map: Map[String, Any] = Map("name" -> "田中", "age" -> 30)
map.toCustomString // => "{name: 田中, age: 30}"
val list: List[Any] = List(1, 2, 3)
list.toCustomString // => "[1, 2, 3]"
(Some(42): Option[Int]).toCustomString // => "42"
(None: Option[Int]).toCustomString // => "nil"
7. コンポーネントパターン¶
トレイトを使って、コンポーネントのライフサイクル管理を実現します。
trait Lifecycle[T]:
def start: T
def stop: T
case class DatabaseConnection(host: String, port: Int, connected: Boolean = false)
extends Lifecycle[DatabaseConnection]:
def start: DatabaseConnection =
println(s"データベースに接続中: $host : $port")
copy(connected = true)
def stop: DatabaseConnection =
println("データベース接続を切断中")
copy(connected = false)
case class WebServer(port: Int, db: DatabaseConnection, running: Boolean = false)
extends Lifecycle[WebServer]:
def start: WebServer =
println(s"Webサーバーを起動中 ポート: $port")
val startedDb = db.start
copy(db = startedDb, running = true)
def stop: WebServer =
println("Webサーバーを停止中")
val stoppedDb = db.stop
copy(db = stoppedDb, running = false)
// 使用例
val db = DatabaseConnection("localhost", 5432)
val server = WebServer(8080, db)
val startedServer = server.start
// データベースに接続中: localhost : 5432
// Webサーバーを起動中 ポート: 8080
val stoppedServer = startedServer.stop
// Webサーバーを停止中
// データベース接続を切断中
8. 条件分岐の置き換え(Strategy パターン)¶
多態性を使って、switch/case 文による型判定を排除できます。
Before(条件分岐)¶
// 悪い例:型による条件分岐
def sendNotificationBad(notificationType: String, message: String, opts: Map[String, String]) =
notificationType match
case "email" => Map("type" -> "email", "to" -> opts("to"), "body" -> message)
case "sms" => Map("type" -> "sms", "to" -> opts("phone"), "body" -> message.take(160))
case "push" => Map("type" -> "push", "device" -> opts("device"), "body" -> message)
case _ => throw new IllegalArgumentException("未知の通知タイプ")
After(多態性)¶
trait NotificationSender:
def sendNotification(message: String): NotificationResult
def deliveryTime: String
case class NotificationResult(
notificationType: String,
to: String,
body: String,
status: String,
subject: Option[String] = None
)
case class EmailNotification(to: String, subject: String = "通知") extends NotificationSender:
def sendNotification(message: String): NotificationResult =
NotificationResult("email", to, message, "sent", Some(subject))
def deliveryTime: String = "1-2分"
case class SMSNotification(phoneNumber: String) extends NotificationSender:
def sendNotification(message: String): NotificationResult =
val truncated = if message.length > 160 then message.take(157) else message
NotificationResult("sms", phoneNumber, truncated, "sent")
def deliveryTime: String = "数秒"
case class PushNotification(deviceToken: String) extends NotificationSender:
def sendNotification(message: String): NotificationResult =
NotificationResult("push", deviceToken, message, "sent")
def deliveryTime: String = "即時"
// ファクトリ関数
def createNotification(notificationType: String, opts: Map[String, String]): NotificationSender =
notificationType match
case "email" => EmailNotification(opts.getOrElse("to", ""), opts.getOrElse("subject", "通知"))
case "sms" => SMSNotification(opts.getOrElse("phone", ""))
case "push" => PushNotification(opts.getOrElse("device", ""))
case _ => throw new IllegalArgumentException(s"未知の通知タイプ: $notificationType")
// 使用例
val email = createNotification("email", Map("to" -> "user@example.com"))
email.sendNotification("重要なお知らせ")
// => NotificationResult("email", "user@example.com", "重要なお知らせ", "sent", Some("通知"))
9. パターンマッチングとトレイトの使い分け¶
| 特徴 | パターンマッチング | トレイト | 型クラス |
|---|---|---|---|
| ディスパッチ | 値に基づく | 型に基づく | 型に基づく |
| 拡張性 | クローズド | オープン | 非常にオープン |
| パフォーマンス | 良い | 最良 | 良い |
| 用途 | ADT、有限の型 | 共通のインターフェース | 既存型への拡張 |
使い分けの指針¶
- Sealed Trait + パターンマッチング: 有限で固定された型のバリエーション
- トレイト: 新しい型が追加される可能性がある場合
- 型クラス: 既存の型(標準ライブラリなど)に振る舞いを追加したい場合
まとめ¶
本章では、Scala における多態性について学びました:
- Sealed Trait: 代数的データ型による有限の型バリエーション
- 複合ディスパッチ: タプルによる複数の値に基づくパターンマッチング
- 階層的ディスパッチ: 継承を使った型階層
- トレイト: 共通インターフェースの定義とミックスイン
- 型クラス: 既存型への振る舞いの追加
- コンポーネントパターン: ライフサイクル管理
- 条件分岐の置き換え: Strategy パターン
これらのメカニズムを適切に使い分けることで、拡張性が高く保守しやすいコードを実現できます。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/scala/part1/src/main/scala/Polymorphism.scala - テストコード:
apps/scala/part1/src/test/scala/PolymorphismSpec.scala
次章予告¶
次章から第2部「仕様とテスト」に入ります。Scala のデータバリデーションと仕様定義について学びます。