hica中的函数式编程
摘要
hica语言中函数式编程的介绍,涵盖表达式、不可变性、纯函数、闭包以及高阶函数(如map、filter和fold)。
暂无内容
查看缓存全文
缓存时间: 2026/07/05 13:31
# hica 中的函数式编程 - hica 源码: https://www.hica.dev/docs/functional-programming/ 函数式编程(FP)是一种通过组合函数来构建程序的方式,而不是编写改变状态的指令序列。hica 的设计围绕这种风格:不可变数据、处处表达式、函数作为一等公民。所有示例均可运行。你不需要函数式编程背景;只要能写函数、能读 match 表达式,就够了。
## 表达式而非语句
大多数语言中,语句“做事情”,表达式“产生值”。在 hica 中,几乎所有东西都是表达式,包括 `if`、`match` 和代码块。这意味着你可以在任何期望值的位置使用它们:
```hica
fun sign(x) => if x < 0 { "negative" } else { "non-negative" }
```
`{}` 代码块的主体是其最后一个表达式的值。无需 `return` 关键字:
```hica
fun clamp(x, lo, hi) {
if x < lo { lo }
else if x > hi { hi }
else { x }
}
```
这个单一规则能带你度过函数式编程的大部分内容:当一切都有值时,一切都可以组合。
## 默认不可变性
函数式编程避免共享的可变状态。当你创建值后不能修改它时,你的函数更容易推理和测试。hica 使用 `let` 进行不可变绑定:
```hica
let name = "Alicia"
let scores = [85, 92, 78]
```
你永远不会就地修改 `scores`。而是创建新列表:
```hica
let updated = scores + [95] // 新列表: [85, 92, 78, 95]
let doubled = map(scores, (x) => x * 2)
```
当你需要可变性(计数器、循环变量)时,使用 `var`。它是局部作用域的,不会泄漏到函数之外:
```hica
var total = 0
for x in scores {
total = total + x
}
```
`var` 是可选加入的。其他所有东西保持不可变。
## 纯函数
**纯函数**对于相同输入总是返回相同输出,且没有副作用(无打印、无文件 I/O、无可变状态)。纯函数易于测试和组合。
```hica
fun add(a, b) => a + b
fun square(x) => x * x
fun to_celsius(f: float) => (f - 32.0) * 5.0 / 9.0
```
在 hica 中,函数默认是纯的。类型系统(继承自 Koka)会追踪 I/O 等效果,因此当函数确实有副作用时,其类型中会体现出来。纯函数是你构建的基础,其余一切皆是组合。
## 函数作为一等公民
在函数式编程中,函数和整数、字符串一样是值。你可以将它们存储在变量中,传递给其他函数,并从函数中返回。
```hica
fun apply(f, x: int) => f(x)
fun main() {
let double = (x) => x * 2
let greet = (name) => "Hello, " + name
println(apply(double, 5)) // 10
println(greet("Olle")) // Hello, Olle
}
```
接受或返回另一个函数的函数称为**高阶函数**。上面的 `apply` 就是一个例子。
## 闭包
**闭包**是捕获其周围作用域中变量的函数:
```hica
fun make_adder(n) => (x) => x + n
fun main() {
let add5 = make_adder(5)
let add10 = make_adder(10)
println(add5(3)) // 8
println(add10(3)) // 13
}
```
每次调用 `make_adder` 都会返回一个新函数。该函数“闭包”了 `n`,意味着即使在 `make_adder` 返回之后,它也能记住创建时的 `n` 值。
这种模式让你能够按需生成特化函数:
```hica
fun make_multiplier(factor) => (x) => x * factor
fun main() {
let triple = make_multiplier(3)
let nums = [1..5]
println(map(nums, triple)) // [3, 6, 9, 12, 15]
}
```
## map, filter, fold
这三个函数涵盖了处理列表的大部分需求。
### map: 变换每个元素
```hica
fun main() {
let nums = [1..5]
println(map(nums, (x) => x * x)) // [1, 4, 9, 16, 25]
println(map(nums, show)) // ["1", "2", "3", "4", "5"]
}
```
`map` 接受一个列表和一个函数。它对每个元素应用该函数,并返回一个等长的新列表。
### filter: 保留匹配的元素
```hica
fun main() {
let nums = [1..8]
let evens = filter(nums, (x) => x % 2 == 0)
println(evens) // [2, 4, 6, 8]
}
```
`filter` 只保留谓词返回 `true` 的元素。
### fold: 归约为单个值
```hica
fun main() {
let nums = [1..5]
let total = fold(nums, 0, (acc, x) => acc + x)
println(total) // 15
let product = fold(nums, 1, (acc, x) => acc * x)
println(product) // 120
}
```
`fold` 累积结果。它从一个初始值开始,依次对每个元素应用函数:`acc` 保存运行中的结果,`x` 是当前元素。
你可以用 fold 实现许多列表操作:
```hica
fun my_length(xs) => fold(xs, 0, (acc, _) => acc + 1)
fun my_max(xs) => fold(xs, 0, (acc, x) => if x > acc { x } else { acc })
fun my_reverse(xs) => fold(xs, [], (acc, x) => [x] + acc)
```
## flatten 和 flat_map
`map` 变换每个元素。但有时你传递给 `map` 的函数本身会返回一个列表。结果是列表的列表,这通常不是你想要的:
```hica
fun main() {
let sentences = ["hello world", "foo bar", "one two three"]
let split_words = map(sentences, (s) => split(s, " "))
println(split_words)
// [["hello", "world"], ["foo", "bar"], ["one", "two", "three"]]
}
```
你想要一个包含所有单词的扁平列表。有两个函数可以解决这个问题。
### concat: 展开一层嵌套
```hica
fun main() {
let nested = [[1, 2], [3, 4], [5, 6]]
println(concat(nested)) // [1, 2, 3, 4, 5, 6]
}
```
`concat` 接受一个列表的列表,展开一层。(其他语言称之为 `flatten`。)
### flat_map: 一步完成 map 和 flatten
`flat_map` 对每个元素应用函数,并将所有结果列表连接在一起。它是 `map` 后接 `concat`,但写成一个步骤:
```hica
fun main() {
let sentences = ["hello world", "foo bar", "one two three"]
let all_words = flat_map(sentences, (s) => split(s, " "))
println(all_words)
// ["hello", "world", "foo", "bar", "one", "two", "three"]
}
```
当你映射的函数返回一个列表时,应该使用 `flat_map`。
### 在管道中展开元素
`flat_map` 在管道中很自然地使用。在单个元素变为多个元素的那一步使用它:
```hica
fun expand(n) => [n, n * 10] // 每个元素扩展为两个
fun main() {
let result = [1, 2, 3]
|> flat_map(expand) // [1, 10, 2, 20, 3, 30]
|> filter((x) => x > 5) // [10, 20, 30]
println(result)
}
```
如果没有 `flat_map`,你会得到 `[[1, 10], [2, 20], [3, 30]]`,而 `filter` 会在列表上操作,而不是数字。
### 同样的思想也适用于 Maybe
模式:*应用一个产生包装值的函数,然后展平包装*,在 Maybe 中同样存在。`map_maybe` 变换 `Maybe` 内部的值。但如果函数本身返回 `Maybe`,你会得到 `Maybe<Maybe<T>>`。`and_then` 通过自动展平这个额外层级来避免这种情况:
```hica
fun parse_pos(s: string) : maybe<int> {
let n = parse_int(s)?
if n > 0 { Some(n) } else { None }
}
fun main() {
// and_then 链接每个返回 Maybe 的步骤——没有嵌套,没有嵌套的 match
let result = Some("42")
|> and_then((s) => parse_int(s)) // 解析字符串 → maybe
|> and_then((n) => parse_pos(show(n)))
|> map_maybe((n) => n * 2)
println(result) // Some(84)
let bad = Some("-5")
|> and_then((s) => parse_int(s))
|> and_then((n) => parse_pos(show(n)))
|> map_maybe((n) => n * 2)
println(bad) // None
}
```
列表的 `flat_map` 和 Maybe 的 `and_then` 是相同的思想,只是名字不同。函数式程序员称这种操作为 *bind*。理解这种模式:“用本身返回包装值的函数映射包装值,并且不要双重包装”,这才是关键,不管什么类型。
## 使用 |> 进行组合
管道运算符 `|>` 将一个表达式的结果送入下一个函数。它从左到右阅读,匹配操作的顺序:
```hica
fun main() {
let result = [1..10]
|> filter((x) => x % 2 == 0) // [2, 4, 6, 8, 10]
|> map((x) => x * x) // [4, 16, 36, 64, 100]
|> fold(0, (acc, x) => acc + x) // 220
println(result)
}
```
没有 `|>` 你会写成:
```hica
let result = fold(map(filter([1..10], (x) => x % 2 == 0), (x) => x * x), 0, (acc, x) => acc + x)
```
有了 `|>`,每一步都在自己的行上,你按照变换发生的顺序阅读。这才是重点:每一步是一个函数,它们串联起来。
### 无点风格
当 lambda 只是直接将参数传递给函数时,你可以完全省略 lambda:
```hica
fun is_even(x) => x % 2 == 0
fun square(x) => x * x
fun main() {
let result = [1..5]
|> filter(is_even) // 等价于 filter(nums, (x) => is_even(x))
|> map(square)
println(result) // [4, 16]
}
```
这种风格(直接命名函数而不是用 lambda 包装)称为**无点风格**。当函数名比 lambda 传达更多信息时使用它。
## 递归
函数式编程使用递归,而命令式代码使用循环。递归函数通过更小的输入调用自身,直到遇到基本情况:
```hica
fun sum(xs) => match xs {
[] => 0,
[x, ..rest] => x + sum(rest)
}
fun main() {
println(sum([1..5])) // 15
}
```
`[x, ..rest]` 模式将列表拆分为第一个元素和其余部分。这自然配合递归使用:
```hica
fun contains(xs, target) => match xs {
[] => false,
[x, ..rest] => x == target || contains(rest, target)
}
fun map_r(xs, f) => match xs {
[] => [],
[x, ..rest] => [f(x)] + map_r(rest, f)
}
```
对于相互递归(两个函数互相调用),不需要前向声明:
```hica
fun is_even(n) => if n == 0 { true } else { is_odd(n - 1) }
fun is_odd(n) => if n == 0 { false } else { is_even(n - 1) }
```
在实践中,你使用 `map`/`filter`/`fold` 的频率远高于显式递归,但递归是处理树形数据的正确工具,了解它有助于你阅读别人的代码。
## 代数数据类型
函数式编程使用**代数数据类型**(ADT)来建模具有不同形态的数据。hica 有两种。
### Structs: 乘积类型
Struct 将字段组合在一起:
```hica
struct Point { x: int, y: int }
struct Person { name: string, age: int }
fun greet(p: Person) => "Hi, {p.name}!"
fun distance(a: Point, b: Point) : int {
let dx = a.x - b.x
let dy = a.y - b.y
dx * dx + dy * dy
}
```
Struct 是不可变的。要“更新”一个字段,创建一个新 struct:
```hica
struct Player { name: string, score: int }
fun add_score(p: Player, points: int) : Player =>
Player { name: p.name, score: p.score + points }
```
### Enums: 和类型
Enum 表示不同变体之间的选择,每个变体可以携带自己的数据:
```hica
type Shape {
Circle(radius: float),
Rect(width: float, height: float),
Point
}
fun area(s: Shape) : float => match s {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
Point => 0.0
}
```
编译器会检查穷尽性:如果你遗漏了一个变体,会收到警告。这使得添加新变体是安全的;编译器会告诉你每个需要更新的地方。
Enum 可以是递归的,这正是建模树形数据的方式:
```hica
type Tree {
Leaf,
Node(value: int, left: Tree, right: Tree)
}
fun tree_sum(t: Tree) : int => match t {
Leaf => 0,
Node(v, l, r) => v + tree_sum(l) + tree_sum(r)
}
```
## Maybe 和 Result
两个内置类型用于处理失败,而无需使用异常。
### Maybe: 可能不存在的值
```hica
fun find_first(xs, pred) => match xs {
[] => None,
[x, ..rest] => if pred(x) { Some(x) } else { find_first(rest, pred) }
}
fun main() {
let nums = [1, 3, 5, 4, 7]
match find_first(nums, (x) => x % 2 == 0) {
Some(n) => println("First even: {n}"),
None => println("No evens found")
}
}
```
`Some(x)` 包装一个值;`None` 表示不存在。编译器强制你处理两种情况。
### 使用组合子进行链式调用
每一步都嵌套 `match` 会很快变得笨拙。组合子保持链式扁平。还有一个微妙之处:当你想应用的函数本身返回 `Maybe` 时,使用 `map_maybe` 会得到 `Maybe<Maybe<T>>`。`and_then` 通过展平一层来防止双重包装,这与 `flat_map` 对列表所做的工作相同:
```hica
fun main() {
// map_maybe 变换内部的值,保留 None
let x = Some(21) |> map_maybe((n) => n * 2)
println(x) // Some(42)
// and_then 链式调用一个本身返回 Maybe 的函数
let y = Some("42")
|> and_then((s) => parse_int(s))
|> map_maybe((n) => n + 1)
println(y) // Some(43)
// 在第一个 None 处短路
let z = Some("nope")
|> and_then((s) => parse_int(s))
|> map_maybe((n) => n + 1)
println(z) // None
}
```
### Result: 成功或特定错误
`Result` 在失败时携带错误信息:
```hica
fun safe_divide(a, b) =>
if b == 0 { Err("division by zero") } else { Ok(a / b) }
fun validate_positive(n) =>
if n > 0 { Ok(n) } else { Err("must be positive") }
fun main() {
let result = safe_divide(100, 4)
|> and_then_result((n) => validate_positive(n))
|> map_result((n) => n * 2)
println(result) // Ok(50)
let bad = safe_divide(100, 0)
|> and_then_result((n) => validate_positive(n))
println(bad) // Err("division by zero")
}
```
### ? 运算符
对于链式许多可能失败步骤的函数,`?` 保持代码扁平。如果某一步失败,它提前返回 `None` 或 `Err`:
```hica
fun add_strings(a: string, b: string) : maybe<int> {
let x = parse_int(a)?
let y = parse_int(b)?
Some(x + y)
}
fun main() {
println(add_strings("10", "32")) // Some(42)
println(add_strings("10", "oops")) // None
}
```
需要知道三个约束条件:
- **返回类型注解是必需的**:编译器需要它才能正确地生成提前返回。
- 包装类型必须匹配:`?` 用于 `maybe` 时只能在使用 `maybe` 返回类型的函数内部,用于 `result` 时同理。
- `?` **不能在 `main()` 中使用**:`main()` 返回 `()`。将可失败的逻辑移到辅助函数中,并在 `main()` 中使用 `match` 调用它。
## 综合应用
一个综合应用了 structs、纯函数、闭包、管道、模式匹配和 `maybe` 的程序:
```hica
struct Student { name: string, grade: int }
fun letter_grade(g: int) : string => match g {
90..=100 => "A",
80..=89 => "B",
70..=79 => "C",
60..=69 => "D",
_ => "F"
}
fun passing(s: Student) : bool => s.grade >= 60
fun summarise(students: list<Student>) {
let passing_students = filter(students, passing)
let names = map(passing_students, (s) => s.name)
let avg = fold(passing_students, 0, (acc, s) => acc + s.grade) / length(passing_students)
println("Passing: {join(names, ", ")}")
println("Average grade (passing): {avg}")
println("Letter grade: {letter_grade(avg)}")
}
fun main() {
let students = [
Student { name: "Alicia", grade: 92 },
Student { name: "Björn", grade: 55 },
Student { name: "Cecilia", grade: 78 },
Student { name: "David", grade: 61 }
]
summarise(students)
}
```
输出:
```
Passing: Alicia, Cecilia, David
Average grade (passing): 77
Letter grade: C
```
## 带走的要点
概念 | hica
------------- | -------------
不可变数据 | 默认 `let`,需要时用 `var`
一等函数 | `(x) => x * 2`、`fun add(a, b) => a + b`
闭包 | `fun make_adder(n) => (x) => x + n`
组合 | `|>` 管道运算符
变换列表 | `map`、`filter`、`fold`
展平/展开 | `concat`(展平)、`flat_map`(map + 展平)
链式包装值 | `and_then`(Maybe)、`and_then_result`(Result),思想同 `flat_map`
递归数据 | `type Tree { Leaf, Node(...) }`
安全失败 | `Maybe`(`Some`/`None`)和 `Result`(`Ok`/`Err`)
穷尽匹配 | `match` 配合编译器检查变体
要点不是避免循环。而是:组合成小函数往往更容易测试、命名和重用。hica 的设计使这成为自然的默认。
相似文章
从第一原理看函数式编程,第1部分——动机
本文从第一原理介绍函数式编程,涵盖函数的数学定义及编程语言范式的分类。这是面向命令式编程者系列文章的第一部分。
函数错位的探索
本文探讨了如何使用去函数化将函数嵌入纯数据中,讨论了Haskell中的类型类和一个玩具级的WASM后端示例。
Prism:一种带类型效应的非纯函数式语言
Prism 是一种新型函数式语言,它结合了代数效应与类型系统,允许在没有单子的情况下使用可变状态及其他效应,同时从外部保持纯函数性。其目标是让效应成为类型系统的一等公民,从而实现优化和安全使用。
Show HN: Hsrs – 用于 Rust 的类型安全 Haskell 绑定生成器
Hsrs 是一个类型安全的 FFI 绑定生成器,允许从 Haskell 调用 Rust 代码,具有自动内存管理、类型转换和 Borsh 序列化功能。它在 Rust 中提供注解,并生成符合语言习惯的 Haskell 包装器。
Simon Jones 谈函数式编程、类型思维与无用语言
Simon Peyton Jones 阐述了函数式编程的数学基础、诸如提高可维护性之类的益处,以及其对主流语言的影响,同时讨论了处理副作用等挑战。