跳轉到

2005:F# 誕生#

事件#

F# 由微軟研究院(Microsoft Research,Cambridge)的 Don Syme 設計,2005 年發表首版,2010 年隨 Visual Studio 2010 正式納入 .NET 平台成為一級語言。F# 以 OCaml 為藍本移植到 .NET:保留了 ML 家族的型別推論、代數資料型別與模式匹配,同時完全相容 .NET 的物件系統與函式庫。Don Syme 先前在 .NET 泛型(Generics,2005 年随 .NET 2.0 落地)的設計中扮演關鍵角色——F# 正是建立在「有泛型的 CLR」之上。

語法/特性加入的理論與實用原因#

1. ML 核心移植到 .NET#

  • 理論原因:Hindley–Milner 型別推論、代數資料型別、模式匹配是經過數十年驗證的理論基礎,理論上可與物件導向型別系統共存。
  • 實用原因:以 OCaml 為藍本可沿用成熟的語意設計;.NET 生態(GUI、資料庫、Web)直接可用,降低採用門檻。
  • 彌補缺陷:OCaml 不能使用 .NET 生態;C# 缺少函數式表達力——F# 兩邊都補上。

JVM/CLR 互操作的 F# 版:型別推論 + .NET 物件系統共存於同一份程式:

// 直接使用 .NET 類別庫,無需橋接層
let dict = System.Collections.Generic.Dictionary<string, int>()
dict.Add("one", 1)
printfn "%d" dict.["one"]

// ML 風格串列處理與 .NET 物件混用
open System
let today = DateTime.Today
let weekdays =
    [1..7]
    |> List.map (fun d -> today.AddDays(float d))
    |> List.filter (fun d -> d.DayOfWeek <> DayOfWeek.Saturday
                          && d.DayOfWeek <> DayOfWeek.Sunday)
// 代數資料型別與模式匹配
type Shape =
    | Circle of float
    | Rect of float * float

let area shape =
    match shape with
    | Circle r -> System.Math.PI * r * r
    | Rect (w, h) -> w * h

2. 列舉式(discriminated unions)與模式匹配#

  • 理論原因:discriminated union 是 ML 的代數資料型別:封閉的「或」型別,配對模式匹配在理論上保證分解的完整性(exhaustiveness)可靜態檢查。
  • 實用原因:編譯器在新增分支時會警告漏處理的情況,重構極安全;這成為 F# 處理領域模型的主流風格。
  • 彌補缺陷:C# 當時只能用類別階層 + instanceof 模擬,冗長且無完整性檢查(C# 直到 2017 年後才補上模式匹配)。

對照組——C# 模擬代數資料型別的冗長寫法:

// C# 2005:類別階層 + 型別檢查模擬 union,漏分支編譯器不警告
abstract class Shape { }
class Circle : Shape { public double R; }
class Rect : Shape { public double W, H; }

double Area(Shape s) {
    if (s is Circle) return Math.PI * ((Circle)s).R * ((Circle)s).R;
    if (s is Rect)   { var r = (Rect)s; return r.W * r.H; }
    throw new ArgumentException();   // 執行期才會發現漏分支
}

F# 的 discriminated union 一個 type 就表達完整的「或」型別,模式匹配還能巢狀解構:

// 巢狀模式匹配:直接解構運算式樹
type Expr =
    | Num of int
    | Add of Expr * Expr
    | Neg of Expr

let rec eval e =
    match e with
    | Num n -> n
    | Neg a -> -eval a
    | Add (Num a, Num b) -> a + b      // 特殊分支:兩個常數相加
    | Add (a, b) -> eval a + eval b

eval (Add (Num 1, Neg (Num 2)))   // -1

3. 非同步工作流程(async workflows)#

  • 理論原因:以計算表示式(computation expression,即單子)包裝非同步邏輯:async { ... } 區塊內可以用看似同步的語法寫非同步程式,理論基礎是單子式組合。
  • 實用原因:2007 年 F# 1.9 就提供了 async workflows,早於任何主流語言的 async/await;I/O 並行(如爬蟲、伺服)不需回呼地獄。
  • 彌補缺陷:C# 當時的非同步程式設計只能用事件與回呼,極易出錯;F# 的 async 直接示範了更好的寫法。

對照組——C# 2005 的 Begin/End 非同步模式,回呼層層嵌套:

// C# 2005:Begin/End 模式,錯誤處理散落各回呼,巢狀越深越難維護
WebRequest req = WebRequest.Create(url);
req.BeginGetResponse(ar => {
    var resp = req.EndGetResponse(ar);
    resp.GetResponseStream().BeginRead(buf, 0, buf.Length, ar2 => {
        int n = resp.GetResponseStream().EndRead(ar2);
        Parse(buf, n);          // 又一層回呼……
    }, null);
}, null);

F# 的 async 工作流程:用 let! 把非同步步驟串成看似同步的直線程式碼,錯誤處理回歸正常的 try/with:

let fetch url =
    async {
        let req = WebRequest.Create(url)
        use! resp = req.AsyncGetResponse()      // let!:等待非同步結果
        use stream = resp.GetResponseStream()
        let! body = stream.AsyncReadToEnd()
        return parse body                        // 錯誤可用 try/with 正常包覆
    }

// 三個網址並行抓取,一行組合
let all = ["https://a.com"; "https://b.com"; "https://c.com"]
         |> List.map fetch
         |> Async.Parallel
         |> Async.RunSynchronously
let urls = ["https://a.com"; "https://b.com"]
let fetchAll =
    urls
    |> List.map (fun u -> async { let! c = download u
                                  return parse c })
    |> Async.Parallel

4. 型別提供者(type providers)#

  • 理論原因:2012 年 F# 3.0 推出型別提供者:在編譯期由外部資料來源(schema、資料庫、Web API)即時「生成」型別,理論上是把型別推論與外部詮釋資料結合的擦除式(erased)機制。
  • 實用原因:讀取 JSON、CSV、資料庫欄位時,IDE 直接提供強型別智慧提示,架構變更即時反映,不需手寫對應層。
  • 彌補缺陷:靜態語言處理動態資料(JSON、資料庫)時的樣板程式碼負擔。

型別提供者的呼叫樣式:宣告一個由 CSV schema 生成的強型別,欄位直接以屬性存取,不需手寫對應層:

// F# 3.0:CSV 型別提供者——編譯期由 Sample.csv 的 schema 生成型別
type Csv = FSharp.Data.CsvProvider<"Sample.csv">

let rows = Csv.Load("data.csv").Rows
for row in rows do
    printfn "%s:%d" row.Name row.Age    // Name、Age 是強型別屬性,
                                          // schema 變更時編譯期即報錯

沒有型別提供者時(如 C# 2005),同樣的工作得手寫解析與對應:

// C#:手寫解析 + 手寫對應類別,schema 變更只能靠執行期測試發現
class Person { public string Name; public int Age; }
foreach (var line in File.ReadLines("data.csv")) {
    var parts = line.Split(',');
    var p = new Person { Name = parts[0], Age = int.Parse(parts[1]) };
}

5. 單位 of measure#

  • 理論原因:在型別層次為數值附上單位(公尺、秒),理論上以擦除式的型別參數實作,不影響執行期效能。
  • 實用原因:單位錯誤(如 1999 年 NASA 火星氣候探測者號的公尺/英尺混淆)可在編譯期被攔截;QUANT 套件文化(金融與科學計算社群)使 F# 在量化金融領域特別受歡迎。
  • 彌補缺陷:所有主流語言的數值型別都不攜帶單位,單位錯誤只能靠測試或人眼檢查。

管線運算子 |> 是 F# 的招牌語法:資料流向與閱讀順序一致,串接出函數式管道:

// |> 串接:資料從左到右流過每個轉換步驟
let result =
    [1..10]
    |> List.filter (fun x -> x % 2 = 0)
    |> List.map (fun x -> x * x)
    |> List.sum          // 220

// 對照巢狀呼叫(閱讀順序與執行順序相反,可讀性差):
let nested = List.sum (List.map (fun x -> x * x) (List.filter (fun x -> x % 2 = 0) [1..10]))
[<Measure>] type m
[<Measure>] type s
let speed = 9.8<m/s>

單位的實戰:單位錯誤在編譯期被攔截,NASA 火星探測者號的公尺/英尺混淆在 F# 中根本無法編譯:

[<Measure>] type m
[<Measure>] type ft
[<Measure>] type s

let gravity = 9.8<m/s^2>
let altitude = 100.0<m>

// altitude + 328.0<ft>          // 編譯錯誤:m 與 ft 單位不符
let altFt = altitude / 3.28084<m/ft>   // 明確轉換才能跨單位

// 距離 / 時間 推論出速度單位,全部在編譯期檢查
let time = 10.0<s>
let velocity = altitude / time        // float<m/s>,單位自動推論

彌補了什麼缺陷#

F# 彌補了「C# 缺少函數式表達力、OCaml 不能用 .NET 生態」的雙向缺陷。它與 C# 產生了著名的互相影響:F# 的 async workflows(2007)直接啟發了 C# 的 async/await(2012,同樣由 Don Syme 周邊的微軟團隊設計),F# 的模式匹配也促成 C# 2017 年後的模式匹配擴展。F# 成為 .NET 平台上函數式程式設計的旗艦語言。

相關條目#