告别一行APL代码

Hacker News Top 新闻

摘要

作者反思了在其体素游戏中使用的一行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 等数组语言的有原则性重新思考

Lobsters Hottest

本文提出了一种有原则性的方法来重新思考 APL 等数组语言,通过将变量建模为输入维度的函数,旨在相较于传统方法提高可读性和错误检查能力。

256行或更少:测试用例最小化

Lobsters Hottest

一篇技术博客文章,描述了作者用约256行Zig代码实现的极简属性测试库,该库具有用于可复现测试用例生成和算法验证的有限随机数生成器。

一些有趣的软件趣闻

Hillel Wayne — Computer Things

一系列有趣且鲜为人知的软件趣闻,包括在康威生命游戏中实现的俄罗斯方块、Vim 的图灵完备性以及反斜杠字符的历史。