第3章: 多態性とディスパッチ¶
はじめに¶
多態性(ポリモーフィズム)は、同じインターフェースで異なる振る舞いを実現する強力な概念です。F# では、判別共用体、パターンマッチング、インターフェース、アクティブパターンという複数のメカニズムで多態性を実現します。
本章では、これらのメカニズムを使い分けて、柔軟で拡張性の高いコードを書く方法を学びます。
1. 判別共用体による多態性(代数的データ型)¶
F# の判別共用体(Discriminated Union)は、有限の型のバリエーションを定義するのに最適です。コンパイラがパターンマッチングの網羅性をチェックしてくれます。
基本的な使い方¶
/// 図形を表す判別共用体
type Shape =
| Rectangle of width: float * height: float
| Circle of radius: float
| Triangle of baseLength: float * height: float
/// 図形の面積を計算する
let calculateArea shape =
match shape with
| Rectangle(w, h) -> w * h
| Circle r -> System.Math.PI * r * r
| Triangle(b, h) -> b * h / 2.0
// 使用例
calculateArea (Rectangle(4.0, 5.0)) // => 20.0
calculateArea (Circle(3.0)) // => 28.27...
calculateArea (Triangle(6.0, 5.0)) // => 15.0
判別共用体の利点¶
- 網羅性チェック: コンパイラがすべてのケースがカバーされているか確認
- 型安全性: 不正な型の値を渡せない
- パターンマッチング: 分岐を簡潔に記述可能
2. 複合ディスパッチ¶
タプルを使ったパターンマッチングで、複数の値に基づくディスパッチを実現できます。
/// 支払い方法
type PaymentMethod =
| CreditCard
| BankTransfer
| Cash
/// 通貨
type Currency =
| JPY
| USD
| EUR
/// 支払い情報
type Payment =
{ Method: PaymentMethod
Currency: Currency
Amount: int }
/// 支払い結果
type PaymentResult =
{ Status: string
Message: string
Amount: int
Converted: int option }
/// 支払いを処理する(複合ディスパッチ)
let processPayment payment =
match payment.Method, payment.Currency with
| CreditCard, JPY ->
{ Status = "processed"
Message = "クレジットカード(円)で処理しました"
Amount = payment.Amount
Converted = None }
| CreditCard, USD ->
{ Status = "processed"
Message = "Credit card (USD) processed"
Amount = payment.Amount
Converted = Some(payment.Amount * 150) }
| BankTransfer, JPY ->
{ Status = "pending"
Message = "銀行振込を受け付けました"
Amount = payment.Amount
Converted = None }
| _, _ ->
{ Status = "error"
Message = "サポートされていない支払い方法です"
Amount = payment.Amount
Converted = None }
// 使用例
processPayment { Method = CreditCard; Currency = JPY; Amount = 1000 }
// => { Status = "processed"; Message = "クレジットカード(円)で処理しました"; ... }
3. 階層的ディスパッチ(型階層の模倣)¶
判別共用体を使って型の階層を定義し、各型に固有の振る舞いを持たせることができます。
/// 口座タイプ
type AccountType =
| Savings
| PremiumSavings
| Checking
/// 口座
type Account =
{ AccountType: AccountType
Balance: int }
/// 利率を取得
let getInterestRate accountType =
match accountType with
| Savings -> 0.02
| PremiumSavings -> 0.05
| Checking -> 0.001
/// 利息を計算
let calculateInterest account =
float account.Balance * getInterestRate account.AccountType
// 使用例
calculateInterest { AccountType = Savings; Balance = 10000 } // => 200.0
calculateInterest { AccountType = PremiumSavings; Balance = 10000 } // => 500.0
calculateInterest { AccountType = Checking; Balance = 10000 } // => 10.0
4. インターフェース(F# のオブジェクト指向機能)¶
F# のインターフェースは、特定の操作セットを定義する契約です。複数のインターフェースを実装できます。
インターフェースの定義¶
/// バウンディングボックス
type BoundingBox =
{ X: float; Y: float; Width: float; Height: float }
/// 描画可能インターフェース
type IDrawable =
abstract member Draw: unit -> string
abstract member GetBoundingBox: unit -> BoundingBox
/// 変換可能インターフェース
type ITransformable<'T> =
abstract member Translate: float * float -> 'T
abstract member Scale: float -> 'T
abstract member Rotate: float -> 'T
インターフェースの利点¶
- パフォーマンス: パターンマッチングより高速な型ベースのディスパッチ
- 明確なコントラクト: 実装すべきメソッドが明示的
- 複数実装: 複数のインターフェースを組み合わせ可能
5. インターフェースを実装するレコード¶
/// 描画可能な長方形
type DrawableRectangle =
{ X: float; Y: float; Width: float; Height: float }
interface IDrawable with
member this.Draw() =
sprintf "Rectangle at (%.1f,%.1f) with size %.1fx%.1f"
this.X this.Y this.Width this.Height
member this.GetBoundingBox() =
{ X = this.X; Y = this.Y; Width = this.Width; Height = this.Height }
interface ITransformable<DrawableRectangle> with
member this.Translate(dx, dy) =
{ this with X = this.X + dx; Y = this.Y + dy }
member this.Scale(factor) =
{ this with Width = this.Width * factor; Height = this.Height * factor }
member this.Rotate(_) = this
/// 描画可能な円
type DrawableCircle =
{ X: float; Y: float; Radius: float }
interface IDrawable with
member this.Draw() =
sprintf "Circle at (%.1f,%.1f) with radius %.1f" this.X this.Y this.Radius
member this.GetBoundingBox() =
{ X = this.X - this.Radius
Y = this.Y - this.Radius
Width = this.Radius * 2.0
Height = this.Radius * 2.0 }
interface ITransformable<DrawableCircle> with
member this.Translate(dx, dy) =
{ this with X = this.X + dx; Y = this.Y + dy }
member this.Scale(factor) =
{ this with Radius = this.Radius * factor }
member this.Rotate(_) = this
// ヘルパー関数(インターフェースメソッドを呼び出しやすくする)
let draw (drawable: IDrawable) = drawable.Draw()
let getBoundingBox (drawable: IDrawable) = drawable.GetBoundingBox()
// 使用例
let rect = { DrawableRectangle.X = 10.0; Y = 20.0; Width = 100.0; Height = 50.0 }
draw rect // => "Rectangle at (10.0,20.0) with size 100.0x50.0"
let circle = { DrawableCircle.X = 50.0; Y = 50.0; Radius = 25.0 }
getBoundingBox circle // => { X = 25.0; Y = 25.0; Width = 50.0; Height = 50.0 }
6. アクティブパターン(既存型への拡張)¶
アクティブパターンは、パターンマッチングを拡張して、任意の変換ロジックを適用できます。
/// 文字列化アクティブパターン(Map用)
let (|MapToString|) (m: Map<string, obj>) =
let pairs = m |> Map.toSeq |> Seq.map (fun (k, v) -> sprintf "%s: %O" k v)
"{" + System.String.Join(", ", pairs) + "}"
/// 文字列化アクティブパターン(リスト用)
let (|ListToString|) (l: 'a list) =
"[" + System.String.Join(", ", l |> List.map string) + "]"
/// 文字列化関数
let stringify value =
match box value with
| :? Map<string, obj> as m ->
let (MapToString s) = m
s
| :? (int list) as l ->
let (ListToString s) = l
s
| :? string as s -> s
| :? int as i -> string i
| null -> "nil"
| _ -> sprintf "%O" value
// 使用例
stringify (Map.ofList ["name", box "田中"; "age", box 30])
// => "{name: 田中, age: 30}"
stringify [1; 2; 3]
// => "[1, 2, 3]"
7. コンポーネントパターン(ライフサイクル管理)¶
インターフェースを使って、コンポーネントのライフサイクル管理を実現します。
/// ライフサイクルインターフェース
type ILifecycle<'T> =
abstract member Start: unit -> 'T
abstract member Stop: unit -> 'T
/// データベース接続
type DatabaseConnection =
{ Host: string; Port: int; Connected: bool }
static member Create(host, port) =
{ Host = host; Port = port; Connected = false }
interface ILifecycle<DatabaseConnection> with
member this.Start() =
printfn "データベースに接続中: %s : %d" this.Host this.Port
{ this with Connected = true }
member this.Stop() =
printfn "データベース接続を切断中"
{ this with Connected = false }
/// Webサーバー
type WebServer =
{ Port: int; Db: DatabaseConnection; Running: bool }
static member Create(port, db) =
{ Port = port; Db = db; Running = false }
interface ILifecycle<WebServer> with
member this.Start() =
printfn "Webサーバーを起動中 ポート: %d" this.Port
let startedDb = (this.Db :> ILifecycle<DatabaseConnection>).Start()
{ this with Db = startedDb; Running = true }
member this.Stop() =
printfn "Webサーバーを停止中"
let stoppedDb = (this.Db :> ILifecycle<DatabaseConnection>).Stop()
{ this with Db = stoppedDb; Running = false }
// 使用例
let db = DatabaseConnection.Create("localhost", 5432)
let server = WebServer.Create(8080, db)
let startedServer = (server :> ILifecycle<WebServer>).Start()
// データベースに接続中: localhost : 5432
// Webサーバーを起動中 ポート: 8080
let stoppedServer = (startedServer :> ILifecycle<WebServer>).Stop()
// Webサーバーを停止中
// データベース接続を切断中
8. 条件分岐の置き換え(Strategy パターン)¶
多態性を使って、switch/case 文による型判定を排除できます。
Before(条件分岐)¶
// 悪い例:型による条件分岐
let sendNotificationBad notificationType message (opts: Map<string, string>) =
match notificationType with
| "email" ->
{| Type = "email"; To = opts.["to"]; Body = message |}
| "sms" ->
{| Type = "sms"; To = opts.["phone"]; Body = message.[..min 159 (message.Length - 1)] |}
| "push" ->
{| Type = "push"; Device = opts.["device"]; Body = message |}
| _ -> failwith "未知の通知タイプ"
After(多態性)¶
/// 通知結果
type NotificationResult =
{ NotificationType: string
To: string
Body: string
Status: string
Subject: string option }
/// 通知送信インターフェース
type INotificationSender =
abstract member SendNotification: string -> NotificationResult
abstract member DeliveryTime: string
/// メール通知
type EmailNotification =
{ To: string; Subject: string }
interface INotificationSender with
member this.SendNotification(message) =
{ NotificationType = "email"
To = this.To
Body = message
Status = "sent"
Subject = Some this.Subject }
member this.DeliveryTime = "1-2分"
/// SMS通知
type SMSNotification =
{ PhoneNumber: string }
interface INotificationSender with
member this.SendNotification(message) =
let truncated = if message.Length > 160 then message.[..156] else message
{ NotificationType = "sms"
To = this.PhoneNumber
Body = truncated
Status = "sent"
Subject = None }
member this.DeliveryTime = "数秒"
/// プッシュ通知
type PushNotification =
{ DeviceToken: string }
interface INotificationSender with
member this.SendNotification(message) =
{ NotificationType = "push"
To = this.DeviceToken
Body = message
Status = "sent"
Subject = None }
member this.DeliveryTime = "即時"
/// 通知を作成するファクトリ関数
let createNotification notificationType (opts: Map<string, string>) : INotificationSender =
match notificationType with
| "email" ->
{ To = opts |> Map.tryFind "to" |> Option.defaultValue ""
Subject = opts |> Map.tryFind "subject" |> Option.defaultValue "通知" }
| "sms" ->
{ PhoneNumber = opts |> Map.tryFind "phone" |> Option.defaultValue "" }
| "push" ->
{ DeviceToken = opts |> Map.tryFind "device" |> Option.defaultValue "" }
| _ -> failwithf "未知の通知タイプ: %s" notificationType
// 使用例
let email = createNotification "email" (Map.ofList ["to", "user@example.com"])
email.SendNotification("重要なお知らせ")
// => { NotificationType = "email"; To = "user@example.com"; Body = "重要なお知らせ"; ... }
9. 式ツリーパターン(Expression Problem の解決)¶
判別共用体とパターンマッチングを使って、式の評価や変換を行います。
/// 式を表す判別共用体
type Expr =
| Num of int
| Add of Expr * Expr
| Mul of Expr * Expr
| Neg of Expr
/// 式を評価する
let rec eval expr =
match expr with
| Num n -> n
| Add(e1, e2) -> eval e1 + eval e2
| Mul(e1, e2) -> eval e1 * eval e2
| Neg e -> -(eval e)
/// 式を文字列化する
let rec exprToString expr =
match expr with
| Num n -> string n
| Add(e1, e2) -> sprintf "(%s + %s)" (exprToString e1) (exprToString e2)
| Mul(e1, e2) -> sprintf "(%s * %s)" (exprToString e1) (exprToString e2)
| Neg e -> sprintf "(-%s)" (exprToString e)
/// 式を簡約する(定数畳み込み)
let rec simplify expr =
match expr with
| Add(Num 0, e) | Add(e, Num 0) -> simplify e
| Mul(Num 0, _) | Mul(_, Num 0) -> Num 0
| Mul(Num 1, e) | Mul(e, Num 1) -> simplify e
| Neg(Neg e) -> simplify e
| Add(e1, e2) -> Add(simplify e1, simplify e2)
| Mul(e1, e2) -> Mul(simplify e1, simplify e2)
| Neg e -> Neg(simplify e)
| e -> e
// 使用例
let expr = Mul(Add(Num 2, Num 3), Num 4)
eval expr // => 20
exprToString expr // => "((2 + 3) * 4)"
simplify (Add(Num 0, Num 5)) // => Num 5
simplify (Mul(Num 1, Num 5)) // => Num 5
Clojure / Scala / F# 比較¶
| 概念 | Clojure | Scala | F# |
|---|---|---|---|
| 代数的データ型 | マルチメソッド | sealed trait | 判別共用体 |
| 複合ディスパッチ | ベクターキー | タプルマッチ | タプルマッチ |
| インターフェース | プロトコル | trait | interface |
| 型階層 | derive | 継承 | 判別共用体 |
| 既存型拡張 | extend-protocol | 型クラス | アクティブパターン |
| ファクトリ | 関数 | コンパニオンオブジェクト | 関数/静的メソッド |
パターンマッチングとインターフェースの使い分け¶
| 特徴 | パターンマッチング | インターフェース |
|---|---|---|
| ディスパッチ | 値に基づく | 型に基づく |
| 拡張性 | クローズド | オープン |
| パフォーマンス | 良い | 最良 |
| 用途 | ADT、有限の型 | 共通のインターフェース |
使い分けの指針¶
- 判別共用体 + パターンマッチング: 有限で固定された型のバリエーション
- インターフェース: 新しい型が追加される可能性がある場合
- アクティブパターン: 既存の型にパターンマッチングを拡張したい場合
まとめ¶
本章では、F# における多態性について学びました:
- 判別共用体: 代数的データ型による有限の型バリエーション
- 複合ディスパッチ: タプルによる複数の値に基づくパターンマッチング
- 階層的ディスパッチ: 判別共用体を使った型階層
- インターフェース: 共通インターフェースの定義と複数実装
- アクティブパターン: 既存型へのパターンマッチング拡張
- コンポーネントパターン: ライフサイクル管理
- Strategy パターン: 条件分岐の置き換え
- 式ツリー: 評価・変換・簡約の実装
これらのメカニズムを適切に使い分けることで、拡張性が高く保守しやすいコードを実現できます。
参考コード¶
本章のコード例は以下のファイルで確認できます:
- ソースコード:
apps/fsharp/part1/src/Library.fs(Polymorphism モジュール) - テストコード:
apps/fsharp/part1/tests/Tests.fs
次章予告¶
次章から第2部「仕様とテスト」に入ります。F# のデータバリデーションと仕様定義について学びます。