编码 · 编程语言
F# 入门教程
简短回答
F# 是 .NET 上函数式优先的静态类型语言。它会为你推断大多数类型,用缩进代替花括号,并把不可变数据、模式匹配和管道运算符 |> 作为常规写法,同时仍能完整使用 .NET 库。安装 .NET SDK 后,用 dotnet fsi hello.fsx 运行脚本。
F# 是什么,谁在用
F# 由微软研究院的 Don Syme 设计,如今是开源语言,由微软和 F# Software Foundation 社区共同维护。它属于 ML 语言家族(与 OCaml 类似),运行在 Windows、macOS 和 Linux 上的 .NET 之上。
团队用 F# 做金融建模、数据分析、业务领域密集的逻辑、Web 后端和脚本。因为它与 C# 编译成同样的 .NET 中间语言,F# 项目可以使用任何 NuGet 包,也可以被 C# 代码调用。
安装 F# 并运行 Hello, world
从微软官网安装 .NET SDK。F# 已包含在内,无需单独安装。做快速实验时,写一个扩展名为 .fsx 的脚本文件,用 F# Interactive(dotnet fsi)运行。
// hello.fsx
printfn "Hello, world!"dotnet fsi hello.fsx
# Hello, world!
dotnet new console -lang "F#" -o HelloApp
cd HelloApp
dotnet run第二组命令会创建带 .fsproj 文件和 Program.fs 的完整控制台项目。在项目中,.fsproj 里列出的文件顺序很重要:一个文件只能使用排在它前面的文件中的代码。
值与基本类型
let 把名字绑定到值。绑定默认不可变,除非你标记为 mutable,此时要用 <- 而不是 = 来修改。
| 类型 | 示例 | 说明 |
|---|---|---|
| int | 42 | 32 位整数 |
| float | 3.14 | 64 位浮点数(.NET 的 double) |
| decimal | 19.99m | 精确的十进制数,适合表示金额 |
| string | "hello" | 不可变的 .NET 字符串 |
| char | 'a' | 单个字符 |
| bool | true, false | 逻辑值 |
| unit | () | “没有值”,类似 void |
let name = "Ana"
let age = 30
let mutable count = 0
count <- count + 1
printfn "%s is %d, count = %d" name age count
// Ana is 30, count = 1
let total = float age + 0.5 // explicit conversion neededF# 从不隐式转换数字:1 + 2.0 是编译错误。请用 float、int、string 等函数转换。
控制流与函数
if 是返回值的表达式,所以两个分支必须是同一类型。循环(for、while)是有的,但大多数代码改用集合函数。
let sign n =
if n > 0 then "positive"
elif n < 0 then "negative"
else "zero"
for i in 1 .. 3 do
printfn "%d squared is %d" i (i * i)
let add x y = x + y // int -> int -> int
let add5 = add 5 // partial application
printfn "%d" (add5 10) // 15
let square = fun x -> x * x
[1; 2; 3; 4]
|> List.filter (fun x -> x % 2 = 0)
|> List.map square
|> printfn "%A" // [4; 16]参数用空格分隔,不用逗号。每个函数一次接收一个参数(柯里化),所以 add 5 会返回一个新函数。管道运算符 x |> f 等价于 f x,让你能从上到下书写数据转换。缩进是有语义的:函数或循环的主体必须缩进在它下面。
集合、记录与 Option
| 集合 | 字面量 | 说明 |
|---|---|---|
| 列表 | [1; 2; 3] | 不可变链表,默认选择 |
| 数组 | [| 1; 2; 3 |] | 大小固定,元素可变,索引快 |
| 序列 | seq { 1 .. 10 } | 惰性,按需计算 |
| 映射 | Map ["a", 1; "b", 2] | 不可变字典 |
| 集 | set [1; 2; 2] | 由唯一值组成的不可变集合 |
type Person = { Name: string; Age: int }
let ana = { Name = "Ana"; Age = 30 }
let older = { ana with Age = 31 } // copy with one change
let people = [ ana; { Name = "Ben"; Age = 17 } ]
let adult = people |> List.tryFind (fun p -> p.Age >= 18)
match adult with
| Some p -> printfn "First adult: %s" p.Name
| None -> printfn "No adults"
// First adult: Ana记录是带命名字段、按结构判断相等的不可变类型。F# 自己的类型没有 null;缺失的值用 Option 表示:Some value 或 None。List.tryFind 这样的函数返回 option,编译器会强制你处理两种情况。
标志性特性:可辨识联合与模式匹配
可辨识联合描述的值恰好是若干具名分支中的一个,每个分支各带自己的数据。match 检查各个分支,如果你漏了一个,编译器会发出警告。
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
let area shape =
match shape with
| Circle r -> System.Math.PI * r * r
| Rectangle (w, h) -> w * h
[ Circle 1.0; Rectangle (2.0, 3.0) ]
|> List.map area
|> List.iter (printfn "%.2f")
// 3.14
// 6.00其他知名特性还有类型推断(很少需要写类型标注)、度量单位(float<m> 不能与 float<s> 相加),以及用于异步代码的计算表达式,如 task { ... } 和 async { ... }。
错误处理
对于预期内的失败,地道的 F# 会返回 Result:Ok value 或 Error info。对于意外失败,可以使用 .NET 异常,即 try ... with、failwith 和 raise。
let parseAge (s: string) =
match System.Int32.TryParse(s) with
| true, n when n >= 0 -> Ok n
| true, _ -> Error "Age must not be negative"
| false, _ -> Error "Not a number"
match parseAge "abc" with
| Ok n -> printfn "Age %d" n
| Error msg -> printfn "Error: %s" msg
// Error: Not a number
let safeDivide a b =
try Some (a / b)
with :? System.DivideByZeroException -> None
printfn "%A" (safeDivide 10 0) // None在 .NET 中 TryParse 有一个 out 参数;F# 把它转成元组 (success, value),可以直接对它做匹配。
一个完整的小程序:词频统计
let text = "the cat and the hat and the bat"
text.Split(' ')
|> Array.countBy id
|> Array.sortByDescending snd
|> Array.truncate 2
|> Array.iter (fun (word, n) -> printfn "%s %d" word n)
// the 3
// and 2Array.countBy id 把相同的单词分组,返回 (word, count) 对;snd 取每一对的第二个元素用于排序;最后用解构元组的 lambda 打印结果。
接下来读什么
- Microsoft Learn:F# 文档,包括“Tour of F#”和语言参考。
- 在
dotnet fsi中练习,可以粘贴代码并立即看到结果;输入以;;结束。 - 学习
List、Array、Seq、Option和Result模块;日常代码大多由它们构成。 - 基础熟练之后,看看 NuGet 上的 F# Web 框架和库。
常见问题
F# 只能在 Windows 上用吗?
不是。F# 运行在跨平台的 .NET 上,所以你可以在 Windows、macOS 和 Linux 上构建和运行 F# 程序。
F# 和 C# 有什么区别?
两者都运行在 .NET 上,可以使用同样的库。C# 以面向对象为主并加入了函数式特性,F# 则函数式优先,默认采用不可变数据、类型推断和模式匹配。
F# 里的 |> 是什么意思?
x |> f 是管道运算符,意思是 f x。它把左边的值作为右边函数的最后一个参数传入,所以一连串转换可以从上到下阅读。
F# 和 OCaml 有关系吗?
有。F# 属于 ML 家族,核心语法与 OCaml 接近,但它是为 .NET 构建的,默认使用有语义的缩进,并有计算表达式等自己的特性。