林子豪的 PKM
← 返回 Blog

强类型 vs 动态类型:AI 生成代码的思考

强类型语言的编译器能做检查,是否意味着 AI 生成代码更有优势?本文探讨了类型表达力、类型体操成本、编译期 vs 运行期验证,以及动态类型语言在 AI 生成场景下的灵活优势。

4 min read
type-systemaiclojurerusthaskellsoftware-engineering

直觉的陷阱

直觉上,强类型语言(Rust、Scala、Haskell)在 AI 生成代码场景下似乎更有优势:

- 编译器能做严格检查

- 类型系统是天然的契约

- 错误在编译期就能发现

但仔细想想,这个直觉可能是错的。

类型表达力的限制

强类型语言的类型系统虽然强大,但表达力是有边界的。

常见的约束无法用类型表达

很多实际的约束,类型系统表达不出来或者表达起来很困难:

- 字符串格式:email、phone number、正则匹配

- 数值范围:年龄必须 ≥ 0,评分必须在 0-100 之间

- 业务规则:用户余额不能小于转账金额,订单状态转换必须符合工作流

- 跨字段约束:开始时间必须早于结束时间

- 外部依赖:数据库 schema 变化、API 响应格式

这些约束如果用类型系统表达,要么做不到,要么要做大量类型体操。

类型体操的成本

为了让类型系统表达更复杂的约束,你会陷入 type gymnastics:

// Rust: 试图用类型表达"字符串必须是非空 email"
struct NonEmptyString(String);

struct Email(String);

impl TryFrom<NonEmptyString> for Email {
    type Error = &'static str;

    fn try_from(s: NonEmptyString) -> Result<Self, Self::Error> {
        // 这里面还是要写验证逻辑
        if is_valid_email(&s.0) {
            Ok(Email(s.0))
        } else {
            Err("invalid email")
        }
    }
}

问题是:

- 每个约束都要写一堆 wrapper

- 类型系统只是"外壳",验证逻辑还是要手写

- 类型层级越深,代码越复杂

- 读懂类型比读懂业务逻辑还难

这就是 "type as theorem, program as proof" 的代价——你需要把所有约束都"证明"给类型系统看,但它并不关心你的业务逻辑,只关心类型推导。

Clojure 的灵活验证

Clojure 原生是动态类型,但这不代表没有验证机制。相反,它的验证生态更灵活。

多种验证方式

编译期 + 运行期

Clojure 的验证不局限于编译期,可以在多个阶段介入:

;; 1. 定义 schema
(def user-schema
  [:map
   [:id :int]
   [:name [:string {:min 1}]]
   [:email [:re email-regex]]
   [:age [:int {:min 0 :max 150}]]
   [:status [:enum :active :inactive :pending]]])

 ;; 2. 编译期:导出给 clj-kondo
 (malli.dev/export-types!)  ;; 生成 clj-kondo config(详见 [malli-cljkondo-type-export](./malli-cljkondo-type-export.md))

;; 3. 运行期:动态验证
(m/validate user-schema input)  ;; 返回 true/false 或 详细错误

;; 4. 运行期:instrument 函数
(malli.dev/instrument! `user-function)

;; 5. 测试期:用 schema 生成测试数据
(gen/generate (m/schema user-schema))

渐进式验证

你可以根据需求选择验证的严格程度:

;; 轻量级:只是描述
(def simple-schema
  [:map [:name :string] [:age :int]])

;; 中等:加一些约束
(def medium-schema
  [:map
   [:name [:string {:min 1}]]
   [:age [:int {:min 0 :max 150}]]])

;; 严格:复杂的业务规则
(def strict-schema
  [:map
   [:name [:string {:min 1} {:max 100}]]
   [:age [:int {:min 0 :max 150} {:error-message "age must be between 0 and 150"}]]
   [:email [:re email-regex {:error/message "invalid email format"}]]
   [:status [:enum {:error/message "invalid status"} :active :inactive :pending]]
   [:created-at inst?]
   [:updated-at [:and inst? [:fn {:error/message "updated_at must be after created_at"}
                                #(t/after? % (get % "created_at"))]]]])

不需要"证明"一切

在强类型语言中,为了通过编译,你必须"证明"所有约束。

在 Clojure 中,你可以选择性地验证:

;; 写代码时,不验证
(defn create-user [name age email]
  {:name name :age age :email email})

;; 需要时,加上验证
(defn create-user-validated [name age email]
  (let [user {:name name :age age :email email}]
    (if (m/validate user-schema user)
      user
      (throw (ex-info "invalid user" (m/explain user-schema user))))))

;; 或者用 instrument 自动验证
(malli.dev/instrument! `create-user-validated)

这种灵活性在 AI 生成代码场景下特别有价值:

为什么灵活验证更适合 AI 生成

1. AI 生成的代码往往不完美

强类型语言中,如果 AI 生成的代码类型不匹配,编译不过。你需要不断调整 prompt 直到类型正确。

Clojure 中,AI 生成基础代码后,你可以:

- 先跑起来看效果

- 然后逐步加上验证

- 或者用 instrument 自动发现问题

2. 渐进式增强更符合开发流程

真实的开发流程是:

- 快速原型(不管类型)

- 加上基础验证

- 根据实际需求加强验证

强类型语言要求一开始就"证明"一切,这和快速迭代矛盾。

3. 运行期验证更实用

很多约束在编译期无法验证:

- 数据库查询结果

- API 响应

- 用户输入

- 外部系统依赖

Clojure 的运行期验证能覆盖这些场景,而强类型语言的类型系统做不到。

4. 错误信息更友好

```clojure

;; Clojure Malli 的错误信息

(m/explain strict-schema {:name "" :age 200 :email "invalid"})

;; => [{:path [:name], :in ["name"], :message "string length must be at least 1"}

;; {:path [:age], :in ["age"], :message "value must be less than or equal to 150"}

;; {:path [:email], :in ["email"], :message "invalid email format"}]

```

对比强类型语言的类型错误:

```rust

error[E0308]: mismatched types

--> src/main.rs:5:20

|

5 | let user = User { name: "", age: 200, email: "invalid" };

| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected Email, found &str

```

Malli 的错误信息更具体,更容易修复。

关键是表达力的权衡

真正的关键不是"静态 vs 动态",而是:

- 你想把多少约束放进类型系统?

- 类型体操的成本 vs 验证灵活性的收益?

强类型语言的选择是:把尽可能多的约束放进类型系统,但代价是复杂的类型定义和证明。

动态类型语言(尤其是 Clojure)的选择是:保持类型系统简单,用灵活的验证机制覆盖更多约束场景。

在 AI 生成代码的场景下,后者可能更实用:

- AI 生成的代码往往不完美

- 快速迭代比完美类型更重要

- 运行期验证能覆盖更多场景

- 渐进式增强更符合开发流程

毕竟,AI 生成代码的目标是快速得到可用的结果,而不是一开始就写出"完美证明"的代码。