2010:Haskell 2010 標準#
事件#
2010 年 7 月,Haskell 委員會發布 Haskell 2010,這是自 Haskell 98 以來十二年間唯一的語言修訂。它刻意保持「小改」:外來函數介面(FFI)正式併入標準、移除 n+k patterns、加入 do-then-else 等細節修正。真正的重點不在標準本身,而在於 Haskell 2010 確立了「穩定核心 + 編譯器擴展」的生態模式,讓 GHC 的擴展文化成為 Haskell 實際上的演進引擎。
語法/特性加入的理論與實用原因#
1. FFI 併入標準#
- 理論原因:與非純函數式世界互操作需要明確標注不純邊界(
unsafe/safe、ccall等)。 - 實用原因:綁定 C 函式庫、作業系統 API 是實務必需,統一規範讓可攜套件成為可能。
- 彌補缺陷:Haskell 98 時各編譯器 FFI 寫法不一,套件難以移植。
FFI 把不純邊界明確寫在型別系統中,IO 型別就是「會碰到真實世界」的標記:
{-# LANGUAGE ForeignFunctionInterface #-}
import Foreign.C.Types
-- 綁定 C 的 sqrt:ccall 標注呼叫約定, unsafe 標注不純邊界
foreign import ccall unsafe "math.h sqrt"
c_sqrt :: CDouble -> CDouble
mySqrt :: Double -> Double
mySqrt = realToFrac . c_sqrt . realToFrac
2. 移除 n+k patterns#
- 理論原因:
f (n+1) = ...這類 pattern 依賴數學記號的語法糖,會使 pattern matching 的語義曖昧且妨礙優化。 - 實用原因:實務中幾乎無人使用,留下來只是歷史包袱。
- 彌補缺陷:語言定義中不必要的複雜性。
3. GHC 擴展文化:穩定核心 + 擴展#
Haskell 2010 之後的型別系統進步幾乎全部透過 GHC 擴展落地,每個擴展都可逐檔開關:
- GADTs(generalized algebraic data types):建構子可指定更精確的回傳型別,可在型別層編碼語法與不變量(如表達式 AST 的型別安全求值器)。彌補 Haskell 98 的 ADT 無法表達「建構結果型別依建構子而異」。
GADT 的經典例子:把「運算式型別」編碼進 AST,求值器不需任何執行期檢查:
{-# LANGUAGE GADTs #-}
data Expr a where
Lit :: Int -> Expr Int
Bool :: Bool -> Expr Bool
Add :: Expr Int -> Expr Int -> Expr Int
eval :: Expr a -> a
eval (Lit n) = n
eval (Bool b) = b
eval (Add x y) = eval x + eval y
-- Add 的兩個子運算式必須是 Expr Int,把 Bool 加進 Add 直接型別錯誤
- Type families:型別層的函數,支撐關聯型別與 GADT 程式設計,讓容器元素型別等關係可在型別層計算。
{-# LANGUAGE TypeFamilies #-}
-- 型別層的函數:容器型別 => 元素型別
type family Elem c
type instance Elem [a] = a
type instance Elem (Map k v) = v
pop :: c -> Elem c -- 回傳型別依容器型別在編譯期算出
pop (x:_) = x
- Template Haskell(
Language.Haskell.TH,2003 起):編譯期元程式設計,巨集與程式碼產生,彌補 Haskell 缺乏 Lisp 式巨集的問題。
{-# LANGUAGE TemplateHaskell #-}
import Language.Haskell.TH
-- 編譯期自動產生 getter:mkLens 'field 會產生存取函式
mkGetter :: Name -> Q [Dec]
mkGetter n = [d| $(conE (mkName ("get" ++ nameBase n)))
= \r -> $(varE n) r |]
- ViewPatterns:在 pattern 中呼叫任意函數,讓 pattern matching 更組合化。
{-# LANGUAGE ViewPatterns #-}
-- 在 pattern 位置直接呼叫 unwords / read 等函數
describe :: String -> String
describe (words -> ["hello", name]) = "打招呼給 " ++ name
describe (readMaybe -> Just (n :: Int)) = "數字 " ++ show n
describe _ = "其他"
- RankNTypes:高階多態(如
forall a. ...出現在參數位置),支撐 ST monad、lens 等核心函式庫。
{-# LANGUAGE RankNTypes #-}
-- lens 的核心型別:s -> a 與 (a -> s -> s) 打包成高一階的多態函數
type Lens s a = forall f. Functor f => (a -> f a) -> s -> f s
-- ST monad 的 runST:型別保證內部可變狀態不會外洩
safeSum :: [Int] -> Int
safeSum xs = runST $ do
return (sum xs) -- 若企圖把 STRef 帶出 runST,RankNTypes 會擋下
- MultiParamTypeClasses / FunctionalDependencies:把 Haskell 98 的 type class 推廣到多參數與型別關係推導。
4. 套件生態:Cabal、Hackage、Stackage#
- 理論原因:模組化與版本依賴需要宣告式的建置描述。
- 實用原因:Cabal(2005)統一建置與依賴格式;Hackage(2007)成為中央套件庫;Stackage(2012)提供經過一起測試的版本快照(LTS),解決相依性地獄。
- 彌補缺陷:Haskell 98 時代缺乏統一的套件系統,程式碼難以分享。
Cabal 檔案以宣告式描述套件、依賴與 GHC 擴展,是 Hackage/Stackage 生態的基礎格式:
-- myapp.cabal:宣告式建置描述
cabal-version: 2.4
name: myapp
version: 0.1.0.0
build-type: Simple
library
exposed-modules: Expr
build-depends: base >=4.12 && <5, containers
default-language: Haskell2010
default-extensions: GADTs, TypeFamilies
彌補了什麼缺陷#
Haskell 2010 彌補了 Haskell 98 的老舊問題:FFI 標準化、語義清理,並以「穩定核心 + 擴展」模式讓業界能用上 GADTs、type families、Template Haskell 等先進型別系統功能,而不需等待委員會修訂。這個模式持續發揮作用:2020 年代的 GHC 9.x 系列帶來多平台支援與 WebAssembly 後端(ghc-wasm-meta,2023 起可用),使 Haskell 能在瀏覽器與邊緣環境執行。