跳轉到

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 能在瀏覽器與邊緣環境執行。

相關條目#