5.2 型別類、trait 與 Functor-Applicative-Monad 實務#
前言:多型不夠用——「操作」也需要多型#
到目前為止的多型都是參數多型(2.1 節):length :: [a] -> Int 對所有 a 一視同仁。但實務常需要依型別分派行為:show :: a -> String 要對 Int、Bool、自訂型別有不同實作;== 要可比較。Haskell 1.x 用 ad-hoc 重載,混亂且不可擴充。1989 年 Wadler 與 Stephen Blott 提出型別類(type class):把「一組操作」定義為型別上的約束,讓編譯器在編譯期分派。這是 ad-hoc 多型的系統化解法,也是 1990s Haskell 對程式語言理論的第二大輸出(第一大是 Monad)。
class Eq a where
(==) :: a -> a -> Bool
instance Eq Int where x == y = primEqInt x y
instance Eq [a] where -- 約束傳播:[a] 可比 ← a 可比
[] == [] = True
(x:xs) == (y:ys) = x == y && xs == ys
注意 instance Eq [a]:編譯器自動推出「可比較元素 ⇒ 可比較列表」。約束傳播使型別類具有代數組合性,這是 Java 介面做不到的(Java 泛型 2004 才補上 T extends Comparable<T> 的受限版本)。
從 Functor 到 Monad:抽象階梯#
Scala trait / Rust trait / Java 介面承接了型別類的角色,而函數式實務把它們用在同一座「抽象階梯」上:
\[ \text{Functor} \;\subset\; \text{Applicative} \;\subset\; \text{Monad} \]
class Functor f where
fmap :: (a -> b) -> f a -> f b -- 「對容器內的每個值套用」
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b -- 「包在容器裡的函數」
class Applicative m => Monad m where
(>>=) :: m a -> (a -> m b) -> m b -- cf. 3.3
三者能力的分界(關鍵在「後續計算能否依賴前面的值」):
| 抽象 | 能做的事 | 實例 |
|---|---|---|
| Functor | 結構不變的映射 | fmap 於 Maybe/列表/Future |
| Applicative | 效果數量固定的組合 | 同時解析多個欄位、zip 型合併 |
| Monad | 後續計算依賴前面結果(效果數量可變) | 鏈式查詢、失敗短路、async |
graph TD
A["Functor<br/>fmap :: (a→b) → f a → f b"] -->|"+ pure 與 <*>"| B["Applicative<br/>固定形狀的效果組合"]
B -->|"+ >>=(依賴前值)"| C["Monad<br/>動態控制流"]
A --> D["法則:identity + composition"]
B --> E["法則:associativity + identity"]
C --> F["法則:left/right identity + associativity(cf. 3.3)"]
style C fill:#bfb,stroke:#333程式碼:Rust trait 實作這座階梯#
Rust 沒有 higher-kinded types,但以 GATs(generic associated types)可部分模擬;以下用最常見的 Result 展示 Monad 實務:
// Functor:map
let r: Result<i32, String> = Ok(3);
let r2 = r.map(|x| x * 2); // Ok(6)
// Applicative 風格:多個 Result 的固定組合
let a = Ok(2) as Result<i32, String>;
let b = Ok(40) as Result<i32, String>;
let c = a.and(b).map(|x| x + 1); // Ok(41)
// Monad 風格:? 運算子 = bind 的語法糖(cf. 3.3)
fn parse_and_sqrt(s: &str) -> Result<f64, String> {
let n: f64 = s.parse().map_err(|e| e.to_string())?; // bind,失敗即短路
if n < 0.0 { Err("negative".into()) } else { Ok(n.sqrt()) }
}
Scala/Cats 的版本更接近 Haskell 原貌:
import cats.implicits._
// Applicative:驗證多個欄位,累積所有錯誤(Monad 做不到!)
case class User(name: String, age: Int)
val v1: ValidatedNel[String, String] = "".validNel // 假設來自輸入
val v2: ValidatedNel[String, Int] = (-1).invalidNel
(v1, v2).mapN(User.apply) // Invalid(NonEmptyList(...)) —— 錯誤同時回報
實務分界:什麼時候用哪一層#
- 能用 Applicative 就別用 Monad:Applicative 結合律使組合可靜態分析(Free Applicative 用於優化、平行發射、解析器疊加); Monad 的依賴性使一切必須序列化。
- 驗證錯誤累積 vs 失敗短路:Monad(
Either的>>=)短路;Applicative(Validated)累積——表單驗證的標準答案。 - Tagless Final 風格(Scala):以
F[_]: Monad抽象效應容器,程式不綁定具體 IO,測試換Id/測試 monad——依賴注入的函數式版本。
歷史意義與後續發展#
- Rust trait(2010s):型別類思想的系統語言化(
Iterator的組合子鏈、?運算子、From/Into),並以 orphan rule 控制實例衝突——直接回應 Haskell 型別類的孤兒實例問題。 - Java/Swift/Kotlin:
Stream.map(Functor)、Optional.map/flatMap(Monad)、flatMap進入 Java 介面——Monad 成為方法名,語法糖與抽象名稱已滲透到最企業的語言。 - Cats / ZIO / fp-ts / Arrow:整個生態以 F-A-M 階梯為綱;TypeScript 的 fp-ts 與 Rust 的
futures都在複製這套詞彙。 - 理論延伸:Functor-Applicative-Monad 之上還有 Traversable、Free Monad(語法樹與直譯器分離——CQRS/事件溯源的理論基礎)與 effect systems(5.3 及 3.3 的後續)。
一句話總結:型別類把「抽象行為」變成可組合、可推導、可查表的一等公民;F-A-M 階梯則是 Moggi 計算理論(3.3)的實務化版本——你今天寫的每個 .map(),都是 1989 年那篇論文的後代。
重點整理#
- 型別類(Wadler & Blott, 1989)= 編譯期分派的 ad-hoc 多型,約束可傳播(
Eq a ⇒ Eq [a])。 - Functor → Applicative → Monad:能力差異在「後續是否依賴前值 / 效果數量是否可變」。
- 各層皆有代數法則;法則使組合可驗證、可最佳化(Free Applicative、fusion)。
- Applicative 可累積錯誤(表單驗證)、可靜態分析;Monad 短路、可動態控制流。
- Rust trait、Scala trait/Cats、Java flatMap 皆為此體系的實務化身。