仓颉语言:把代数数据类型和效应处理器做成一等公民的编译型新语言

2026-05-12 15 预计阅读时间:1 分钟
来源:infoq.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:10 分钟

华为编程语言实验室负责人 Dan Ghika 教授最近公开介绍了仓颉(Cangjie)——一门定位对标 Java、Kotlin、Swift 的应用开发语言。它已经开源,目前在中国超过 80 所高校开设了课程。和主流对手相比,仓颉最显眼的差异在于两件事:代数数据类型(ADT)和效应处理器(Effect Handlers)不是后加的库,而是语言内核的一部分

这两项特性组合起来,对日常写业务代码的开发者到底意味着什么?下面拆开看。

代数数据类型:不只是"增强版枚举"

Java 的枚举可以挂字段和方法,但没法直接表达"这个值要么是 A 携带一个 Int,要么是 B 携带一个 String"这种分支结构。Kotlin 的 sealed class 做到了分支穷举,但语法负担不小。仓颉的 ADT 更接近 Rust 和 Swift 的 enum——声明简洁,模式匹配强制穷举。

一个典型的仓颉 ADT 声明和匹配:

// 定义一个表示网络请求结果的 ADT
enum Result<T> {
    | Ok(T)
    | Err(String)
}

// 使用模式匹配处理分支——编译器会检查是否穷举
func handleResponse(resp: Result<Int64>): Int64 {
    match (resp) {
        case Ok(value) => value
        case Err(msg)  => {
            println("请求失败: ${msg}")
            0  // 返回默认值
        }
    }
}

// 调用
let okResp: Result<Int64> = Ok(200)
let errResp: Result<Int64> = Err("连接超时")
println(handleResponse(okResp))   // 200
println(handleResponse(errResp))   // 请求失败: 连接超时  0

关键点:match 必须覆盖所有分支,漏掉任何一个编译器直接报错。这比 Java 的 switchdefault 只在运行时炸掉要安全得多。对于业务中大量存在的"成功/失败"、"空/有值"、"加载中/已完成/出错"这类状态,ADT 把隐式的 if-else 逻辑变成了显式的类型约束。

效应处理器:把副作用从函数签名里拉出来

效应处理器是仓颉最区别于 Java/Kotlin/Swift 的特性。传统做法里,副作用(异常、日志、异步、状态修改)要么藏在函数体里不透明,要么靠回调、Promise、异常体系间接表达。效应处理器走的是另一条路——副作用本身变成可声明、可拦截、可替换的计算单元

一个简化的效应处理器示例,演示如何拦截"日志输出"效应:

// 声明一个效应类型
effect Log {
    func info(msg: String): Unit
}

// 业务函数声明自己会使用 Log 效应
// 效应出现在函数签名里调用方一眼可见
func processOrder(orderId: String): String \ Log {
    Log.info("开始处理订单: ${orderId}")
    // ... 业务逻辑 ...
    Log.info("订单处理完成")
    orderId
}

// 效应处理器——决定效应的实际行为
handler LogHandler {
    // 拦截 Log.info实际打印到控制台
    func info(msg: String): Unit {
        println("[LOG] ${msg}")
    }
}

//  handler 包裹调用效应有了具体实现
func main(): Int64 {
    with (LogHandler) {
        processOrder("ORD-20240601")
    }
    0
}

运行 main() 输出:

[LOG] 开始处理订单: ORD-20240601
[LOG] 订单处理完成

效应处理器的威力在于同一个业务函数可以搭配不同 handler 产生不同行为。测试时挂一个收集日志到列表的 handler,生产时挂一个写到文件的 handler,函数本身不用改一行。这比往每个函数里注入 Logger 对象、或者靠 AOP 框架织入要直接得多。

组合实战:用 ADT + 效应处理器写一个健壮的配置加载器

把两个特性放在一起用,才能看出仓颉的设计意图——ADT 管数据结构的确定性,效应处理器管副作用的可控性。

// 配置来源的 ADT——明确表达三种可能
enum ConfigSource {
    | File(String)       // 文件路径
    | EnvVar(String)     // 环境变量名
    | Default            // 使用默认值
}

// 读取可能失败的 ADT
enum LoadResult {
    | Success(Map<String, String>)
    | FileNotFound(String)
    | ParseError(String)
}

// 声明两个效应:文件读取和日志
effect ReadFile { func read(path: String): String }
effect Log      { func info(msg: String): Unit }

// 业务函数:声明所有用到的效应,签名完全透明
func loadConfig(src: ConfigSource): LoadResult \ { ReadFile, Log } {
    match (src) {
        case File(path) => {
            Log.info("从文件加载: ${path}")
            try {
                let content = ReadFile.read(path)
                // 简化:假设解析成功
                Success(parseToMap(content))
            } catch (_ => FileNotFound(path))
        }
        case EnvVar(name) => {
            Log.info("从环境变量加载: ${name}")
            // 简化处理
            Success(Map<String, String>())
        }
        case Default => {
            Log.info("使用默认配置")
            Success(defaultMap())
        }
    }
}

// 测试用的 handler——不读真实文件,不打真实日志
handler TestHandler {
    func read(path: String): String {
        "key=value\nport=8080"  // 返回固定测试内容
    }
    func info(msg: String): Unit {
        // 测试时不输出,只记录到断言可检查的地方
    }
}

// 测试调用——零外部依赖
func testLoadConfig(): LoadResult {
    with (TestHandler) {
        loadConfig(File("/fake/path/config.cfg"))
    }
}

这段代码的核心收益: - ConfigSource ADT 让调用方不可能忘记处理"默认配置"这个分支。 - loadConfig 的签名 \ { ReadFile, Log } 把副作用摆在了明面上——读文件、写日志,没有隐藏的惊喜。 - 测试时换一个 handler,整个函数变成纯逻辑验证,不需要 mock 框架、不需要临时文件。

上手之前需要想清楚的几件事

仓颉目前处于早期生态阶段,做技术选型时要正视几个现实:

优势面 - ADT + 效应处理器的组合在主流应用语言里确实没有直接竞品。Rust 有 ADT 但没有效应处理器;Kotlin 有 sealed class 但副作用管理靠协程和库。仓颉把这两件事做进了语言层,理论上有更低的表达成本。 - 编译型语言,运行性能预期对标 Java/Kotlin 而不是解释型脚本。 - 华为投入 + 80+ 高校开课,意味着国内会有持续的人才供给和文档产出。

风险面 - 生态还在建设期:第三方库、IDE 插件、构建工具链的成熟度远不及 Java/Kotlin/Swift。 - 效应处理器对多数团队是新概念,学习曲线比"换个语法写 if-else"要陡。如果团队没有函数式编程背景,上手需要刻意练习。 - 开源后的社区治理节奏和长期维护承诺,需要持续观察。

一个务实的起步路径 1. 在非核心模块做试点——比如一个内部工具的配置加载或日志模块,用仓颉写一个独立组件。 2. 先吃透 ADT 和模式匹配,效应处理器放到第二步。前者上手快、收益直观;后者需要更多设计思考。 3. 关注仓颉官方仓库的包管理和工具链更新,早期阶段这些比语言特性本身更影响开发体验。


仓颉不是"又一个语法略有不同的 Java"。ADT 让数据建模从隐式约定变成编译器强制检查,效应处理器让副作用从函数体内的黑箱变成签名上的白盒。这两件事单独看都不新鲜,但做进同一门编译型应用语言的内核,确实给日常业务代码提供了一条不同于 Java/Kotlin/Swift 的路径。值不值得走,取决于你对生态成熟度的容忍度和对类型安全/副作用控制的重视程度。


相关推荐