Brunost:一个强制使用挪威语 Nynorsk 编写代码的编程语言——深度解析与实战
作者:程序员茄子
发布时间:2026年4月18日
一、引言:语言不只是工具,也是文化
编程语言的世界从来不缺"Hello World"新选手。但大多数新语言无非是换个语法糖,或者在性能与开发体验之间做不同的取舍。你见过用挪威语 Nynorsk 书写全部代码的编程语言吗?
今天要聊的这个语言叫 Brunost——它由挪威开发者 John Mikael Lindbakk(GitHub @atomfinger)创作,以挪威传统文化中极负盛名的棕色山羊奶酪(Brunost)命名。更重要的是,这是一个强制要求开发者使用 Nynorsk 书写变量、函数名和所有代码标识符的函数式编程语言。
如果你是挪威人,你可能会觉得这是一个玩笑。但如果你是程序员,你可能会意识到——这件事背后的工程挑战和文化意义,远比它看起来要有趣得多。
二、文化背景:Nynorsk 是什么?
在深入技术细节之前,我们需要理解 Nynorsk 是什么。
挪威有三种官方语言变体:Bokmål(书面挪威语)、Nynorsk(新挪威语) 和 Sami(萨米语)。其中:
- Bokmål 是大多数挪威人日常使用的书面语言,受丹麦语影响很深
- Nynorsk 则是一种基于挪威方言的规范化书面语言,历史上被视为"挪威语的更'纯粹'形式"
- Sami 是原住民语言
Nynorsk 的一个有趣特性是:它基本上是一种纯书面语言。没有人说"纯正的 Nynorsk",因为即使在 Nynorsk 区域,人们说的也是当地方言。这意味着 Nynorsk 天然适合作为一个只有规范没有"母语者"的形式化系统——这和编程语言何其相似。
Brunost 的创造者正是抓住了这个类比:一个只有规范没有母语者的语言,正好可以用来写一个编译器。
三、Brunost 语言核心特性
3.1 强制 Nynorsk:字典即语法检查器
Brunost 的核心创新在于:它内置了一个 Nynorsk 词典,运行时强制检查所有标识符是否来自 Nynorsk 词汇表。
这意味着:
- 变量名
fart✅(Nynorsk:速度) - 变量名
speed❌ 编译报错:Feil: Namnet er ikkje gyldig nynorsk - 函数名
kalkulerFart✅ - 函数名
calculateSpeed❌
这不仅仅是噱头——它意味着 Brunost 实际上在运行时做了一件很有意思的事:它把语言词汇表从 ASCII token 扩展到了一个完整的自然语言子集。
3.2 基础语法结构
变量声明(可变 vs 不可变):
-- 不可变变量(Nynorsk: fast)
fast fart er 80
fart er 50 -- ❌ 编译错误:Kan ikkje endra ein uforanderleg variabel
-- 可变变量(Nynorsk: endreleg)
endreleg fart er 80
fart er 50 -- ✅ 正常运行
fast 对应英语的 const(不可变),endreleg 对应 var/let(可变)。关键词全部是 Nynorsk,这在代码可读性上造成了一种奇特的效果——懂 Nynorsk 的挪威人看到代码会感觉像在读母语说明书。
条件判断:
bruk terminal
fast fartsgrense er 80
fast minFart er 90
viss (minFart erStørreEnn fartsgrense) gjer {
terminal.skriv("For høy fart!")
} ellers viss (minFart erSameSom fartsgrense) gjer {
terminal.skriv("Farten din er på grensa!")
} ellers {
terminal.skriv("Farten din er innafor.")
}
关键 Nynorsk 词汇:
viss= 如果(if)ellers= 否则(else)gjer= 做/执行(do)erStørreEnn= 大于erSameSom= 等于erMindreEnn= 小于erSameEllerMindreEnn= 小于等于
函数定义:
bruk terminal
gjer kalkulerFart(distanse, timer) {
gjevTilbake distanse / timer
}
fast distanse er 50
fast timer er 2
terminal.skriv("Din fart er: " + kalkulerFart(distanse, timer) + "km/t")
gjer= 定义(do/define)gjevTilbake= 返回(give back)
这里 Brunost 的有趣之处在于:顺序敏感。你必须在使用函数之前声明它,否则编译器会拒绝。这种设计反映了创造者的一种"强观点"——开发者应该知道代码的执行顺序,而不是依赖编译器的提升(hoisting)。
3.3 循环结构
-- forEach 循环
bruk terminal
fast fylke er ["Vestland", "Rogaland", "Troms", "Finnmark"]
forKvart namn i fylke {
terminal.skriv("Hei frå " + namn + "!")
}
-- while 循环
bruk terminal
endreleg teljar er 1
medan (teljar < 4) gjer {
terminal.skriv(teljar)
teljar er teljar + 1
}
forKvart ... i= for each ... inmedan= 当/当...时(while)
3.4 标准库模块
bruk terminal -- 输入输出
bruk matte -- 数学运算
bruk streng -- 字符串处理
bruk liste -- 列表操作
bruk prosess -- 进程管理
所有模块名都是 Nynorsk 词汇。开发者也可以创建自己的模块:
bruk logikk -- 导入同级目录的 logikk 模块
bruk kode.logikk -- 导入子目录 kode 下的 logikk 模块
3.5 异常处理
bruk terminal
gjer del(første, andre) {
viss (andre er 0) gjer {
kast "Kan ikke dele på null"
}
gjevTilbake første / andre
}
prøv {
del(10, 0)
} fang(feil) {
terminal.skriv("Noe feilet: " + feil)
}
prøv= 尝试(try)fang= 捕获(catch)kast= 抛出(throw)
创造者对此有一段经典评论:"有些人说 errors-as-returns 比异常更好。为什么?就别产生错误就好了。简单。"
3.6 WebAssembly 支持
Brunost 解释器可以编译为 WebAssembly,这意味着你可以在浏览器中直接运行 Brunost 代码。创造者做了一个在线 Playground,可以直接体验:
bruk terminal
terminal.skriv("Hei, verda!")
在线体验地址:lindbakk.com(浏览器中直接运行)
创造者的原话:"Wasm 这部分内容主要是为了 SEO 而加的,希望它有用。"
四、技术架构深度解析
4.1 为什么用 Zig 编写解释器?
Brunost 解释器用 Zig 编写。选择 Zig 的理由是:
- 性能接近 C:Zig 没有隐藏的内存分配,性能极高
- 简单直接的内存管理:没有垃圾回收器,对解释器这种需要精细控制内存的场景非常友好
- 与 C 的互操作性:未来如果需要 FFI(外部函数接口),Zig 可以无缝集成 C 库
- 编译产出极小:解释器本身可以被 Wasm 编译,产出极小的二进制
以下是 Zig 解释器的核心设计思路(伪代码):
// 伪代码:Brunost 解释器核心
const std = @import("std");
pub fn eval(ast: AstNode, ctx: *Context) !Value {
switch (ast.kind) {
.fast_decl => {
const val = try eval(ast.value, ctx);
try ctx.defineConst(ast.name, val);
return Value{ .void = {} };
},
.endreleg_decl => {
const val = try eval(ast.value, ctx);
try ctx.defineVar(ast.name, val);
return Value{ .void = {} };
},
.function_def => {
const func = Function{
.params = ast.params,
.body = ast.body,
.closure = ctx.capture(),
};
try ctx.define(ast.name, func);
return Value{ .void = {} };
},
// ...
}
}
Zig 的 try/catch 错误处理机制和 Brunost 的 prøv/fang 异常处理在设计哲学上有一种奇妙的呼应——两者都强调显式错误处理。
4.2 Nynorsk 词典的设计
Nynorsk 词典是 Brunost 系统中最关键也是最复杂的组件之一。它不仅包含单词列表,还需要处理:
- 词形变化:Nynorsk 有丰富的词形变化规则(变格、变位)
- 复合词:挪威语允许通过组合单词形成复合词
- 允许的借词:某些英语词汇在 Nynorsk 中也是合法的
词典的数据结构设计大致如下:
// 伪代码:词典结构
const Dict = struct {
// 基础词汇集合(已验证的 Nynorsk 词汇)
base_words: std.StringHashMap(WordInfo),
// 词缀规则(Nynorsk 词形变化规则)
affix_rules: []AffixRule,
// 允许的借词(来自其他语言的合法词汇)
loan_words: std.StringHashMap(LoanWordInfo),
};
const WordInfo = struct {
root: []const u8, // 词根
category: WordCategory, // 词性(名词/动词/形容词等)
forms: []const []const u8, // 各种变位形式
};
创造者提到的一个有趣限制:BMI(身体质量指数)这个例子在开发过程中被迫移除了,因为 "BMI" 在 Nynorsk 词典中不是合法词汇。
4.3 模块系统和文件系统布局
Brunost 的模块系统遵循一个清晰的路径映射规则:
prosjekt/
├── hovud.brunost -- 主入口
├── logikk.brunost -- 同级模块
└── kode/
└── logikk.brunost -- 子模块
bruk logikk -- 导入同级
bruk kode.logikk -- 导入子模块
这种设计非常简洁,没有 npm/pip 那样的复杂包管理逻辑,但也意味着没有版本控制、没有依赖解析。对于一个"实验性"的语言来说,这完全是合理的工程决策。
五、实战代码:斐波那契数列与 Conway 生命游戏
5.1 斐波那契数列
bruk terminal
gjer rekke(grense) {
viss (grense < 2) gjer {
gjevTilbake grense
}
gjevTilbake rekke(grense - 1) + rekke(grense - 2)
}
endreleg teljar er 1
medan (teljar erSameEllerMindreEnn 15) gjer {
terminal.skriv(rekke(teljar))
teljar er teljar + 1
}
输出:
1
1
2
3
5
8
13
21
34
55
89
144
233
377
610
5.2 FizzBuzz
bruk terminal
bruk matte
endreleg teljar er 1
medan (teljar erSameEllerMindreEnn 15) gjer {
viss (matte.modulus(teljar, 15) erSameSom 0) gjer {
terminal.skriv("FizzBuzz")
} ellers viss (matte.modulus(teljar, 3) erSameSom 0) gjer {
terminal.skriv("Fizz")
} ellers viss (matte.modulus(teljar, 5) erSameSom 0) gjer {
terminal.skriv("Buzz")
} ellers {
terminal.skriv(teljar)
}
teljar er teljar + 1
}
5.3 Conway 生命游戏
bruk liste
bruk terminal
bruk matte
bruk prosess
bruk streng
fast breidde er 60
fast høgd er 30
-- 创建随机初始状态
gjer lagBrett() {
endreleg brett er []
endreleg rad er 0
medan (rad erMindreEnn høgd) gjer {
endreleg rekkje er []
endreleg kolonne er 0
medan (kolonne erMindreEnn breidde) gjer {
-- 随机填充 0 或 1
endreleg tilfeldig er matte.floor(matte.tilfeldig() * 2)
rekkje.push(tilfeldig)
kolonne er kolonne + 1
}
brett.push(rekkje)
rad er rad + 1
}
gjevTilbake brett
}
注:生命游戏在在线版本中因为 Wasm 的 I/O 限制,运行效果不太理想,这是创造者已知的限制。
六、性能分析:Zig 解释器 + Wasm 的优势
6.1 解释器性能
由于 Brunost 是解释型语言(不是编译型),性能主要由解释器的执行效率决定。Zig 的以下特性对性能至关重要:
| 特性 | 优势 |
|---|---|
| 无隐藏内存分配 | 解释器核心循环性能稳定,无 GC 停顿 |
| Comptime(编译时计算) | 词法分析器在编译期完成部分解析 |
| SIMD 支持 | 列表/数组操作可受益于 Zig 的 SIMD intrinsics |
| 跨平台 Wasm 编译 | 一次编译,到处运行 |
6.2 Wasm 部署架构
Brunost 源代码 (.brunost)
↓
Zig 解释器(编译目标)
↓
Wasm 二进制 (~200-500KB)
↓
浏览器 / Node.js / Deno
Zig 对 Wasm 的支持已经相当成熟,可以直接编译为 wasm32-wasi 目标。对于一个需要跨平台部署的解释器来说,这是一个非常优雅的方案。
6.3 已知性能瓶颈
- 字典查询:每次标识符解析都需要在 Nynorsk 词典中查找,这是解释器中额外的 O(n) 开销
- 词形变化:如果词典包含变位形式的完整列表,存储开销较大
- Wasm I/O:WebAssembly 的沙箱限制使得标准 I/O 操作需要特殊处理
七、Brunost 的工程哲学:强观点与刻意限制
Brunost 创造者John Mikael Lindbakk 在项目中体现了一系列强观点:
7.1 "代码是写给人看的,顺便给机器执行"
大多数编程语言的设计哲学是"降低机器的理解成本"(性能优先)或者"降低人的理解成本"(可读性优先)。Brunost 则更进一步:它要求你用一种特定自然语言来表达代码,这使得代码的"文化语义"变得非常重要。
fast fart er 80 不只是声明了一个不可变的数值变量——它还传达了挪威文化中"速度"的概念(fart)。
7.2 刻意限制是设计的核心
创造者明确表示:
- 不会有完整的生态系统
- 不会有复杂的包管理器
- 这是一个"有趣的小项目"
这种刻意限制反而让 Brunost 更像一件完整的艺术品,而非一个半途而废的项目。它不是要取代 Go/Rust/TypeScript,它只是要回答一个问题:如果一门语言强制使用一种非英语的自然语言来书写,会发生什么?
7.3 US 键盘布局的"意外代价"
创造者自己在使用 US 键盘布局时,发现输入 Nynorsk 字符(æ/ø/å)极其困难。这揭示了一个有意思的工程现实:语言的语法和它的输入方式是不可分割的。如果你打算推广 Brunost,你最好有一块挪威键盘,或者花时间配置输入法。
八、Brunost 的局限性:为什么它不适合生产
8.1 词典的局限性
- Nynorsk 词典不可能覆盖所有现代技术词汇
- 技术术语(如 HTTP、API、JSON)需要额外处理
- 不同地区的 Nynorsk 方言差异可能引发兼容性问题
8.2 缺乏工具链
- 没有语法高亮插件(目前)
- 没有 Language Server Protocol(LSP)实现
- 没有调试器
- 没有单元测试框架
创造者开放了 GitHub 仓库,欢迎社区贡献编辑器插件、LSP 等。但这些都需要时间和精力投入。
8.3 性能限制
解释型语言的固有局限,加上 Nynorsk 词典查询的开销,使得 Brunost 不适合计算密集型场景。
九、社区参与:如果你想贡献
创造者明确表示欢迎社区 PR,并给出了几个"低风险"贡献方向:
| 方向 | 难度 | 价值 |
|---|---|---|
| 编辑器语法高亮插件 | ⭐⭐ | VS Code / Vim / Neovim / Helix 支持 |
| Language Server Protocol | ⭐⭐⭐⭐ | 智能提示、跳转、错误诊断 |
| 标准库扩展 | ⭐⭐ | 补充更多实用模块 |
| 文档完善 | ⭐ | README / 教程 / 示例 |
| Wasm I/O 优化 | ⭐⭐⭐ | 改善在线版本体验 |
GitHub 仓库地址:github.com/atomfinger/brunost
十、总结:Brunost 教会我们什么?
Brunost 不是一个"应该被用于生产"的语言。它的创造者自己也说了:"你可以用它……但你不应该。"
但它给我们带来了几个重要的思考:
1. 编程语言的"元设计"
大多数编程语言将词汇表限制在 ASCII/Unicode token,而 Brunost 将其扩展到了一个完整的自然语言子集。这是一种极端的"可读性实验"。
2. 文化与工程的交汇
编程语言不只是技术工件,它们也承载着文化偏见。英语作为编程语言的主导语言是一个历史偶然,而不是必然选择。Brunost 用一种幽默但严肃的方式挑战了这个假设。
3. Zig 作为语言实现工具的价值
选择 Zig 编写解释器展示了 Zig 在工具链开发领域的成熟度——高性能、跨平台、Wasm 支持、简洁的内存管理,使得 Zig 成为一个理想的语言实现平台。
4. "刻意小" 的工程美学
在一个追求"功能完备"的行业里,Brunost 选择了一个有边界的项目,然后把这个边界做到极致。这是一种值得尊敬的工程态度——不是所有东西都需要成为平台。
参考链接
- Brunost 官方介绍:https://lindbakk.com/blog/introducing-brunost
- GitHub 仓库:https://github.com/atomfinger/brunost
- 在线体验:https://lindbakk.com(浏览器 Wasm 版本)
Tags: Brunost | Nynorsk | 编程语言 | Zig | 函数式编程 | WebAssembly | Gleam | 自然语言编程
Keywords: Brunost | Nynorsk programming language | Zig interpreter | Gleam | WebAssembly | functional programming | natural language programming | 挪威语编程 | 编程语言设计