hica中的函数式编程

Hacker News Top 工具

摘要

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 的设计使这成为自然的默认。

相似文章

函数错位的探索

Lobsters Hottest

本文探讨了如何使用去函数化将函数嵌入纯数据中,讨论了Haskell中的类型类和一个玩具级的WASM后端示例。

Prism:一种带类型效应的非纯函数式语言

Lobsters Hottest

Prism 是一种新型函数式语言,它结合了代数效应与类型系统,允许在没有单子的情况下使用可变状态及其他效应,同时从外部保持纯函数性。其目标是让效应成为类型系统的一等公民,从而实现优化和安全使用。