hica 引入函数式编程方式
速览
文章讲解了 hica 这一技术框架/语言中函数式编程的核心概念与实际应用,包括高阶函数、不可变数据等特性。通过示例说明如何用函数式风格编写更简洁、可维护的代码。对希望提升编程范式理解的开发者有一定参考价值。
AI 深度解读
背景
函数式编程(Functional Programming, FP)是一种通过组合函数来构建程序的编程范式,与传统的指令式编程(通过改变状态的指令序列)形成对比。近年来,随着并发、数据密集型应用的增长,函数式编程因其不可变性、无副作用和易于推理的特性重新受到关注。hica 是一门围绕函数式风格设计的新语言,它借鉴了 Koka 的类型系统,强调表达式、不可变数据、高阶函数和纯函数。原文来自 Hacker News,作者通过大量可运行的示例,系统性地介绍了 hica 中函数式编程的核心概念,旨在帮助即使没有 FP 背景的读者快速上手。
核心内容
hica 的函数式编程设计围绕几个核心原则展开:一切皆表达式、默认不可变、纯函数默认、函数作为一等公民,以及强大的组合工具(如 map/filter/fold、flat_map、管道运算符等)。以下是对原文要义的完整翻译和梳理。
表达式,而非语句
在多数语言中,语句执行动作,表达式产生值。在 hica 中,几乎一切(包括 if、match 和代码块 {})都是表达式。这意味着可以在任何期望值的位置使用它们。例如:
fun sign(x) => if x < 0 { "negative" } else { "non-negative" }
{} 块体的值是其中最后一个表达式的值,无需 return 关键字:
fun clamp(x, lo, hi) {
if x < lo { lo }
else if x > hi { hi }
else { x }
}
这条规则贯穿大部分 FP:当一切都有值时,一切都可以组合。
默认不可变
FP 避免共享可变状态。创建值后不能修改它,函数就更容易推理和测试。hica 使用 let 进行不可变绑定:
let name = "Alicia"
let scores = [85, 92, 78]
不能原地修改 scores,而是创建新列表:
let updated = scores + [95] // 新列表: [85, 92, 78, 95]
let doubled = map(scores, (x) => x * 2)
当确实需要可变性(如计数器、循环变量)时,使用 var——它局部作用域并且不能泄漏到函数外:
var total = 0
for x in scores {
total = total + x
}
var 是自愿选择(opt-in)的,其余一切保持不可变。
纯函数
纯函数对相同的输入总是返回相同的输出,并且没有副作用(无打印、无 I/O、无可变状态)。纯函数易于测试和组合。例如:
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 等效果,因此当函数具有副作用时,其类型中会体现出来。纯函数是你构建程序的基础,其余都是组合。
函数作为一等值
在 FP 中,函数如同整数、字符串一样是值。可以将它们存储在变量中、传递给其他函数,或从函数中返回。
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 就是其中之一。
闭包
闭包是捕获其周围作用域变量的函数:
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 每次返回一个新函数。该函数“闭合(closes over)”了 n,意味着即使在 make_adder 返回后,它仍会记住创建时的 n 值。这种模式可以按需定制专用函数:
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:变换每个元素。接受一个列表和一个函数,将函数应用到每个元素,返回等长的新列表。
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"] } -
filter:保留匹配的元素。仅保留谓词返回
true的元素。fun main() { let nums = [1..8] let evens = filter(nums, (x) => x % 2 == 0) println(evens) // [2, 4, 6, 8] } -
fold(归约):将列表缩减为单个值。需要一个初始值和一个函数(累加器
acc+ 当前元素x)。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 实现许多列表操作,如
my_length、my_max、my_reverse。
flatten 与 flat_map
map 变换每个元素,但如果传给 map 的函数本身返回一个列表,结果就会变成列表的列表,通常不是想要的结果。例如将句子拆分成单词:
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:将一层嵌套展平。
concat接受列表的列表,折叠一层(其他语言中常称为flatten)。let nested = [[1, 2], [3, 4], [5, 6]] println(concat(nested)) // [1, 2, 3, 4, 5, 6] -
flat_map:一步完成 map 和 flatten。它应用函数到每个元素,然后连接所有结果列表。
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 自然地融入管道。在某个步骤中一个元素变成多个元素时使用它:
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 >
