diff options
| author | eltrac <hi@eltr.ac> | 2026-10-05 15:40:59 +0800 |
|---|---|---|
| committer | eltrac <hi@eltr.ac> | 2026-10-05 15:40:59 +0800 |
| commit | 5db71115546937741273efdaf11b78064d9ecd77 (patch) | |
| tree | 624070bd6832f30dc343d302f34938bd67d89f4f | |
| parent | f444c5198d7093ea919c7c7d2f913fbe91394df1 (diff) | |
[draft] update
| -rw-r--r-- | content/weekly/101.md | 57 |
1 files changed, 56 insertions, 1 deletions
diff --git a/content/weekly/101.md b/content/weekly/101.md index b0e9df48..34f2977e 100644 --- a/content/weekly/101.md +++ b/content/weekly/101.md @@ -265,6 +265,47 @@ Jenson 认为开源用户也是在意 UI/UX 体验的。他记得之前向 KDE æ > [!note] 📜 > [Why Functional Programming Makes Complex Systems Easier to Build](https://flexiana.com/hidden/why-functional-programming-makes-complex-systems-easier-to-build) +这大概率是一篇用于优化网站 SEO,持续给网站产出内容进行引流的文章,换句话说,类似国内的“营销号”。不过也出于其营销性质,文章简单易读。配图虽然有脱裤子放屁之嫌,但还算能看。本文讲解函数式编程范式和高阶函数(High-order function)如何让复杂系统更容易构建和维护,与传统的以数据为中心的面向对象编程做了对比。 + +文章的主要观点是,函数式系统的数据流向非常清晰,由一个函数流向下一个函数,没有共享的可变状态。在传统的系统里,往往需要维护各种状态,同一块数据有多个写者读者,很难知道是谁修改了数据,数据流很容易变得混乱。 + + + +在面向对象的系统里,对象不仅保存了**不可见**的内部状态,还定义了一系列方法。随着需求增加,越来越多的方法和复杂度会被添加到同一个对象内部。函数式系统由小巧的函数组成,不和对象绑定,天然地容易践行开闭原则(Open-close principle),即对拓展开放,对修改封闭。函数可以很轻易地组合复用,不需要修改函数内部的结构。我想到了面向对象设计中的装饰器模式(decorator pattern),用一个对象包装另一个对象,拓展其功能,其实,只需要用一个函数调用另一个函数就能实现装饰器。 + +纯函数不维护内部状态,只是接收参数,所以每个函数都是独立的,每个函数都能被**测试**。如果要测试一个对象,需要先实例化对象,这可能还需要实例化其他的对象,传给这个对象的构造方法才行,复杂的业务逻辑经常需要这么干。测试函数就不一样了,可以轻易地传入假的测试数据,函数的返回值也清晰且可预测,测试断言写起来也容易。 + +为了方便函数的组合复用,高阶函数往往很有用。这是文中给的例子,`process-transaction` 是个高阶函数,或者说高度抽象的函数。具体的费率、计算策略等细节,都作为参数传入,高阶函数本身不知道。 + +```clojure +(defn process-transaction [transaction calculate-fee] + (let [fee (calculate-fee (:amount transaction))] + (assoc transaction :fee fee))) + +(def payment + (process-transaction {:amount 100} #(* % 0.02))) + +(def refund + (process-transaction {:amount 100} #(* % 0.01))) +``` + +我觉得这个例子不太合理,应该把 `payment` 和 `refund` 各自写成 `process-payment` 和 `process-refund` 函数,它们都调用 `process-transaction` 函数,但在内部写好部分参数,只接受 `{:amount 100}` 这一条数据,相当于是“继承”了 `process-transaction` 函数。 + +上面这个例子中的 `transaction` 是个映射(map),Clojure 的数据管线常常需要传递各种映射,修改其中的值或者增加新值,然后返回,传递给下一个函数。我一般是用箭头宏 `->` 写数据管线的,这篇文章让我学到了新写法,用返回函数的函数构造管线函数: + +```clojure +(defn pipe [& functions] + (fn [value] + (reduce (fn [result fn*] (fn* result)) value functions))) + +(def process-data + (pipe validate transform save)) + +(process-data input) +``` + +这样一来,管线就是数据驱动(data-driven)的,可以随意修改传给 `pipe` 的函数数量和顺序,就像操作数据一样修改管线的结构,而不是写死成函数调用。 + ### 怂货式编程 > [!note] 📜 @@ -295,7 +336,21 @@ Grishaev 认为,如果即将传给函数的参数是 `nil`,那这个函数æ ;; 如果 get-urser-id 返回 nil,get-user 不会被调用 ``` -说实话我是读了这篇文章才第一次知道 `some->` 这个函数,Clojure 标准库里的函数不少,要全部了解应该很难。 +说实话我是读了这篇文章才第一次知道 `some->` 这个函数,Clojure 标准库里的函数不少,常用的就几个,要全部了解应该很难。 + +不仅不应该避免函数报错,反而应该更多地报错,给出明确的报错,这样才能排查问题。Clojure 是动态类型语言,很少有类型报错。可以用 Pre-condition 和 Post-condition 约束函数的输入输出类型,本质上是一系列谓词函数,在调用函数之前用 `:pre` 谓词函数检查参数是否符合要求,返回值之前用 `:post` 检查输出是否正确。如果有返回 `false` 的,就报 `AssertionError`。 + +```clojure +(defn double-number [x] + {:pre [(number? x)]} + (* x 2)) +``` + +函数内部还可以用 `assert` 断言类型。在大部分编程语言里,断言是用于测试的,不该在生成环境中报断言错误。所以在构建时可以把全局变量 `*assert*` 设为 `nil`,这样就不会执行断言。 + +Clojure 还有 `clojure.spec` 库,用来定义更复杂的数据规格(Spec),可以检查和断言数据是否合规,与 Pre/post-condition 组合使用最佳。 + +我最近在写 [weepinbell](https://codeberg.org/eltrac/weepinbell) 的时候也遇到了不少类型问题带来的麻烦,一部分原因是我或多或少也应用了怂货式编程,另一部分原因是测试覆盖不完全,或许还有一部分原因是==动态类型语言需要比静态类型语言更多的自律==。我也开始怀疑,是不是选用静态类型的 Haskell 或 [TypedClojure](https://github.com/typedclojure/typedclojure) 会更好。 --- |
