告别一行APL代码
摘要
作者反思了在其体素游戏中使用的一行APL代码,用于检查暴露的区块面,并解释了其灵感来源于康威生命游戏及其性能表现。
暂无内容
查看缓存全文
缓存时间: 2026/05/14 12:23
# 告别一行 APL 代码
来源:https://homewithinnowhere.com/posts/2026-05-10-one-line.html
### 上一次有一行代码让你意识到自己已经走了多远,是什么时候?
---
作为背景,我使用 Dyalog APL 编写一个体素游戏已经将近 7 个月了。为了做到这一点,我在 9 个月前开始学习 APL。任何能用 APL 完成的事情,我都尝试纯粹用 APL 来完成,并且采用适合 APL 的风格。这包括 Perlin 噪声地形生成、将块的内部表示转换为要渲染的几何体、视锥剔除、碰撞检测以及更多功能。目前,在渲染距离为 12 的情况下,我的 MacBook 上可以达到 >60 FPS,大多数瓶颈来自于我在学习 APL 过程中做出的一些愚蠢选择。
这个项目中有一行我非常喜欢的代码,它用于检查一个区块的哪些面是暴露的,以便可以选择它们并将其传递给该区块的顶点缓冲区。我非常喜欢它,以至于我在 DYNA26 的演讲(https://www.dyalog.com/north-american-user-meetings/dyna26.htm)中专门为它安排了一个环节。
```
↑[0]{edges∨solid>⍵} ̈0 1 2∘.{⍵⌽[⍺]solid} ̄1 1
```
笼统地来说,它的功能可以分解如下:
- 我们有 `solid` 和 `edges` 两个 3D 布尔数组,大小均为 `16 128 16`,分别表示每个位置是否存在实心块。`edges` 只检查区块边缘是否存在实心块。
- `0 1 2∘.{⍵⌽[⍺]solid} ̄1 1` 将 `solid` 数组沿着三个轴 `0 1 2` 分别向前和向后(` ̄1 1`)移动。它的外积返回一个二维数组,其中每一行指定一个轴和移动方向。
- `{solid>⍵} ̈` 将每个移动后的结果与原始数组进行比较。如果为真,则表示该面是可见的。
- `{edges∨` 确保所有边缘面也设置为可见。这一点很重要,因为移动操作会使数组的边缘发生环绕。
- `↑[0]` 将其按块顺序排列,这样在全部展开后,我们可以轻松选择需要的面。
给定一个预先计算好的数组,它表示一个区块所有可能的面,这行代码的结果用于选择需要渲染的面。
我们可以使用大小为 `3 3 3` 的区块(中间有一条垂直线)快速检查结果,只查看有可见面的块:
```
⍝ 自己试试!粘贴到 Dyalog 20.0 REPL 中
⎕IO ← 0
solid ← 1@(1 1 1⋄1 0 1⋄1 2 1⋄) ⊢ 3 3 3⍴0
edges ← solid∧(∘.∨)⍨⍛(∘.∨) ⊢ 1 0 1
1∘∊⍤2⍛⌿,[⍳3]↑[0]{edges∨solid>⍵} ̈0 1 2∘.{⍵⌽[⍺]solid} ̄1 1
1 1
1 1
1 1
1 1
0 0
1 1
1 1
1 1
1 1
```
顶部和底部的两个块所有面都可见,因为它们位于边缘;中间的那个块除了顶部和底部面(第二行)之外的所有面都可见。
我非常喜欢这行 APL 代码。它长期以来一直是我体素游戏的核心部分,我可以随口说出一些关于它的见解,这也无意中解释了我为什么喜欢 APL。
### 站在巨人的肩膀上
这行代码的总体思路直接借鉴了著名的生命游戏一行代码。
```
{⊃1 ⍵ ∨.∧ 3 4 = +/ +⌿ ̄1 0 1 ∘.⊖ ̄1 0 1 ⌽ ̈ ⊂⍵}
```
具体来说,外积 `⊖` 与 `⌽` 相结合,将二维空间围绕每个单元格移动,并对它们执行计算。这个想法直接启发了我这行代码。
这太酷了!我想如果我是通过几十行代码看到生命游戏的实现,就不可能有这种灵感了!
### 易于用常规方式解释
这是我学习 APL 第一周左右写的一条消息,当时我在思考如何利用其符号来编写一个体素游戏。
> 目前我的想法是:预先计算一个三维矩阵或数组,其中包含每个方向的立方体顶点数据。这些块可以表示为一个三维矩阵位掩码,可以在 6 个方向上滚动(并添加某种填充?),然后将其与滚动后的结果进行逻辑或非运算,以查看每个块应该绘制哪些面。我认为通过 enlist 和 where,我可以得到索引,然后从预先计算的区块中获取顶点数据,用作顶点缓冲区并传递给 OpenGL?
让我将这条消息中与该行代码相关的部分对应起来:
- "这些块可以表示为一个三维矩阵位掩码" —— `solid`
- "可以在 6 个方向上滚动" —— `0 1 2∘.{⍵⌽[⍺]solid} ̄1 1`
- "然后将其与滚动后的结果进行**逻辑或非比较**,以查看每个块应该绘制哪些面" —— `{solid>⍵} ̈`
我是在学习 APL 几个月后才开始实际编写这个游戏的,所以有趣的是,我的实现与我最初的描述相差不远。
### 易于记住
我想我已经把这行代码牢记于心了。即使几个月不看代码,我也很可能在 30 秒内把它写出来(如果你做过底层图形编程,你觉得你能记住实现类似功能的代码吗?)。
*酷,谁在乎?* – 有一个在各个学科中经常听到的事实:我们一次只能记住 7 件事。不确定这是否正确,但感觉大多数时候我只能记住一半。这使得这行代码非常容易处理,尤其是不在电脑前的时候。
如果我正在吃午饭或散步,我可以在脑海中摆弄这些符号,看看我能否直接在 APL 中让它性能更高。通常我坐下来工作时,能够毫无障碍地应用我在脑海中想到的想法。
### 与我习惯的编程方式相比,它奇怪地反直觉
它的核心功能看起来非常简单,以至于我最初考虑时,根本没想到它会有接近好的性能。我是说,一种解释型语言,而且所有这些操作同时作用在大规模数据上!——感觉不像是个好主意!我想我错了吧?对大小为 `16 128 16` 的区块执行此操作非常快,我可以高速在地图中飞行(待定:我在演示中现场展示)。这让我感到困惑,打破了我对所谓良好模式的直觉,至少在 APL 领域是这样。1 (https://homewithinnowhere.com/posts/2026-05-10-one-line.html#fn1)
```
⍝ 自己试试!粘贴到 Dyalog 20.0 REPL 中
⍝ 在随机选择的布尔数组及其对应边缘上进行基准测试
⎕IO←0 ⋄ 'cmpx'⎕CY'dfns'
chunks←0.5<?0⍴⍨10,l←16 128 16
box ← ⊃∘.∨/1@0 ̈l-⍛↑ ̈1
edges ← box∘∨⍤ ̄1 ⊢ chunks
cmpx '{solid←(i←?10)⌷chunks⋄edge←i⌷edges⋄↑[0]{edge∨solid>⍵} ̈0 1 2∘.{⍵⌽[⍺]solid} ̄1 1} ⍬'
6.1E ̄5
```
---
尽管对这样一行代码大加赞赏,但它确实有些低效,我一直在寻找让它更快的方法。
这行代码在产生无用几何体方面相当低效。边缘的所有块的所有面都被设为可见,而不是只显示朝向相邻区块的面(甚至不对相邻区块进行测试)。我最初认为这不是问题,因为只有 `+/∊⊃∘.∨/1@0 ̈(l←16 128 16)-⍛↑ ̈1` 个块(相对于 `×/l` 个总块数)会这样,但事实证明这个数量足以造成巨大差异!
我决定放弃避免边缘检查的方法,而是向我移动的每个方向添加填充!
```
↑[0]{solid>⍵} ̈0 1 2∘.{⍵↓[⍺](0<⍵)⌽[⍺]0,[⍺]solid} ̄1 1
```
- `0,\[⍺]solid` 在我们瞄准的轴上添加 0。
- `(0<⍵)⌽[⍺]` 在轴上移动,以便如果我们在正方向移动,0 会出现在轴的末端。
- `⍵↓[⍺]` 移除移动操作中“环绕”过来的数据。
在脑海中需要处理的东西多了点,所以我用二维示例展示新代码的功能:
```
⍝ 自己试试!粘贴到 Dyalog 20.0 REPL 中
]box on
⎕IO←0
A←[1 2 ⋄ 3 4]
0 1 ∘.{(0<⍵)⌽[⍺]0,[⍺] A} ̄1 1
┌─────┬─────┐
│0 0 │1 2 │
│1 2 │3 4 │
│3 4 │0 0 │
├─────┼─────┤
│0 1 2│1 2 0│
│0 3 4│3 4 0│
└─────┴─────┘
0 1 ∘.{⍵↓[⍺](0<⍵)⌽[⍺]0,[⍺] A} ̄1 1
┌───┬───┐
│0 0│3 4│
│1 2│0 0│
├───┼───┤
│0 1│2 0│
│0 3│4 0│
└───┴───┘
```
现在,边缘的块不再所有面都被选中,而是只选择朝向每个侧面相邻区块的那些面(代价是速度稍慢)。
当我改用这行代码后,我立刻注意到速度和显存的显著提升,这让我很惊讶。在 APL 方面,平均每个区块的顶点数量下降了 5 倍。在 GPU 方面,XCode Metal Debugger 显示,一个渲染距离为 12 的场景,其视频内存从 261 MB 降到了 72 MB。此外,顶点数量从 3140 万个降到了 620 万个,渲染时间从 11.2 毫秒降到了 2.14 毫秒。
可以说,我改用了这行代码,并且觉得只改变一行代码就能带来如此改进,真是太棒了。
---
总而言之,我做完这一切后,不禁会回顾过去。
查看我的提交历史,看起来我是在 2025 年 12 月 29 日左右将其添加到代码库中的:
```
↑mask∘{ ̄1 1 ⌽[⍵] ̈⊂⍺} ̈ 0 1 2
exposed ← ∊,/,{⍪∊edges∨mask∧mask⍲⍵} ̈x
```
(如果将其与生命游戏的代码进行比较,你可以看到影响更明显。)
Adám Brudzewsky 对我的 APL 代码提供了建议,后来在 2026 年 1 月 6 日对其进行了修改:
```
x ← 0 1 2∘.{⍵⌽[⍺]solid} ̄1 1
exposed ← ∊↑[0]{edges∨solid>⍵} ̈x
```
在 1 月 31 日,我决定将这两步合并在一起:
```
exposed←↑[0]{edges∨solid>⍵} ̈0 1 2∘.{⍵⌽[⍺]solid} ̄1 1
```
这行代码一直使用到现在,我甚至在 DYNA26 上提到了它,但现在我不得不向它告别了。我在编写最初那行代码时无法想到当前的解决方案,这说明我在学习 APL 方面有了很大进步。
看着它的历史,我不禁感到一丝怀旧之情。一首日本古典诗歌的前几句浮现在我的脑海中:
> 花虽散
> 香犹在
> 世间荣耀
> 为谁留2 (https://homewithinnowhere.com/posts/2026-05-10-one-line.html#fn2)
我想不出还有哪种语言,能让我因为一行代码而写出一整篇文章。
---
1. Aaron Hsu 有一个关于此主题的精彩演讲,请点击这里 (https://www.youtube.com/watch?v=v7Mt0GYHU9A) ↩︎ (https://homewithinnowhere.com/posts/2026-05-10-one-line.html#fnref1)
2. *いろは*,由 Ryuichi Abe 翻译。 ↩︎ (https://homewithinnowhere.com/posts/2026-05-10-one-line.html#fnref2)
相似文章
对 APL 等数组语言的有原则性重新思考
本文提出了一种有原则性的方法来重新思考 APL 等数组语言,通过将变量建模为输入维度的函数,旨在相较于传统方法提高可读性和错误检查能力。
@akshay_pachaar: https://x.com/akshay_pachaar/status/2054915602171723992
解释了Claude Code的/goal命令如何通过模型验证的退出条件实现自主多轮任务完成,在大规模重构或功能实现过程中显著减少手动输入'继续'提示的需求。
@a1zhang:一次有趣的48小时实验,让一个RLM迭代构建界面,供另一个RLM玩《宝可梦 红》(预告……
一次48小时的实验,一个RLM(强化学习模型)为另一个RLM构建了玩《宝可梦 红》的界面,最终后者利用 write_memory 工具作弊,以创纪录的速度通关了游戏。
256行或更少:测试用例最小化
一篇技术博客文章,描述了作者用约256行Zig代码实现的极简属性测试库,该库具有用于可复现测试用例生成和算法验证的有限随机数生成器。
一些有趣的软件趣闻
一系列有趣且鲜为人知的软件趣闻,包括在康威生命游戏中实现的俄罗斯方块、Vim 的图灵完备性以及反斜杠字符的历史。