用 Ruby 词法记号长度背诵圆周率:解析 TRICK 2015 金奖作品 kinaba/entry.rb 的 Piphilology 技巧
2026/9/13 5:42:02 网站建设 项目流程

用 Ruby 词法记号长度背诵圆周率:解析 TRICK 2015 金奖作品 kinaba/entry.rb 的 Piphilology 技巧

【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby

导读

本文深入剖析 Ruby 官方仓库 sample/trick2015/kinaba/ 下获得 TRICK 2015(第 2 届 Transcendental Ruby Imbroglio Contest,即 RubyKaigi 上的 Ruby 黑魔法大赛)金奖的参赛作品entry.rb。该作品以 "Best piphilology"(最佳圆周率记忆法)为名,实现了一个"用 Ruby 词法记号(lexical token)长度编码圆周率数字"的极致脑洞:程序本身既是 Pi 的记忆口诀,运行时又能输出 10000 位圆周率。读完本文,你将理解 Ripper 词法分析驱动的 Pi 编码原理、srand返回值作为"隐式赋值槽"的黑魔法、以及 TRICK 4096 字符限制下的程序嵌入策略。

TRICK 2015 与获奖作品背景

sample/trick2015/README.md明确说明该目录收录了第 2 届 TRICK(Transcendental Ruby Imbroglio Contest for rubyKaigi)的获奖作品,并特别强调"这些是反面教材(BAD EXAMPLES),绝不能把它们当作示例代码来学习 Ruby"。

其中 kinaba/entry.rb 获得Gold award(金奖),获奖称号为 "Best piphilology"。整个目录结构如下:

  • kinaba/entry.rb:Best piphilology —— 金奖
  • ksk_1/entry.rb:Most unreadable ALU —— 银奖
  • monae/entry.rb:Doubling amphisbaena award —— 铜奖
  • eregon/entry.rb:Least general solver —— 第 4 名
  • ksk_2/entry.rb:Most general solver —— 第 5 名

所有作品以 MIT 许可证发布。TRICK 的玩法核心在于:参赛者必须在代码中同时满足"程序能正常运行完成特定任务"与"代码满足某种极端的自指约束"双重条件,而本文主角的约束就是每个词法记号的长度必须对应 π 的数字序列

Piphilology:用词长记忆圆周率

英文世界的历史传统

remarks.markdown首先指出,这个程序是一种面向 Rubyist 的Piphilology(圆周率记忆法)。在英语世界,背诵圆周率的诗歌遵循一个规则:第 n 个单词的字母数就是 π 的第 n 位数字。经典的开头是

"How I wish I could calculate pi" → 3, 1, 4, 1, 5, 9 ...

即每个单词的长度依次对应 3-1-4-1-5-9。当某位数字为0时,使用一个 10 字母的单词来表示(因为 10 % 10 = 0)。

Ruby 的翻译:词法记号长度即数字

作者把这一传统移植到 Ruby:源代码中每个词法记号(token)的字符长度对 10 取模,就得到 π 的一个数字。注意这里的关键是"词法记号"而非"单词"——因为 Ruby 的空白符(空格、换行、缩进)不影响分词,所以整段源码可以任意换行排版,而 Pi 的编码完全由 token 本身决定。

文档中给出了验证方法,用标准库 Ripper 对源文件做词法分析:

$ ruby -r ripper -e \ 'puts Ripper.tokenize(STDIN).grep(/\S/).map{|t|t.size%10}.join' < entry.rb 31415926535897932384626433832795028841971693993751058209749445923078164062862...

这里Ripper.tokenize返回源码的全部词法记号(来自仓库中的 ext/ripper/lib/ripper/lexer.rb 与 ext/ripper/lib/ripper/core.rb 等实现),grep(/\S/)过滤掉纯空白 token,再对每个 token 求长度并取模 10,拼起来恰好是 π 的前缀。也就是说,整个程序的排版,本质上就是一篇用 Ruby 关键字、类名、常量、运算符"填字"写成的 Pi 长诗

同时,直接运行程序也能得到同样的输出——它在计算并打印前 10000 位 π:

$ ruby entry.rb 31415926535897932384626433832795028841971693993751058209749445923078164062862...

程序运行要求

运行方式非常简单,无需任何参数:

$ ruby entry.rb

作者确认的实现/平台:

  • ruby 2.2.3p173 (2015-08-18 revision 51636) [x64-mingw32]

也就是说这是在 2015 年的 CRuby 2.2.3(Windows 64 位构建)上验证通过的。由于程序依赖的srand返回值语义、Ripper 分词规则以及Integer等 Kernel 方法都属于语言核心行为,现代 CRuby 上基本同样可以运行,但若要严格复现当年的验证环境,应以该版本为准。

内部原理:Token 长度的硬约束

词法记号不是简单的空格分词

remarks.markdown特别强调了一个微妙点:token 并不是按空格切分得到的最小单元。例如表达式

a*b + cdef

并不表示数字序列 [3, 1, 4](即 "a*b" 3 字符、"+" 1 字符、"cdef" 4 字符),实际上 Ripper 会将其切分为 5 个 token:a*b+cdef,对应 [1, 1, 1, 1, 4]。

这意味着什么?一旦你决定写一个多字符的标识符,后续连续的非空白字符都会被独立计数。这种"token 长度税"对代码写作施加了极其严苛的约束:你不能自由选择变量名长度,因为每个字符都在 π 的编码序列里"消费"掉一个位置;不能随意写注释(注释里的词也会被计入);字符串字面量的内容长度也会影响编码。这是整个作品最核心的工程难点——所有代码逻辑都必须在"每个 token 长度已由 π 的某一位数字事先规定好"的前提下进行。

理论上可行,实践中困难

文档随即指出一个深刻的观察:π 被(广泛)认为是一个正规数(normal number),即任何有限数字序列都会在其小数展开中出现(参见 Wikipedia 的 Normal number 条目)。如果这一猜想成立,那么从理论上讲,任何程序都可以被"藏进" π 的某段数字中——你只需要把程序分词后的 token 长度序列当作一段数字串,去 π 里找到它的出现位置即可。

但在实践中这远非易事,尤其是在 TRICK 的4096 字符代码长度限制下。文档给出了量化的论证:假设你想嵌入某个程序片段,其 token 长度序列(比如g += hij对应的 [1, 2, 3])在 π 中随机均匀出现,那么期望每隔 1000 位才出现一次;而为了到达那个位置,前面的 500 万个填充字符(平均约 5000 字符/位 × 1000 位)早已远超 4096 字节的限额。如果老老实实按"找到位置再填充"的思路来写,代码体积会爆炸到原来的 333 倍以上(文档估算 blowup 为 1000/3)。因此必须使用一些 TRICK 级别的技巧来压缩开销。

四个关键黑魔法

remarks.markdown的 Internals 一节公开了作者的四个核心技巧:

1. 全局变量别名(alias $a $b)

alias $curTerm $initTerm

Ruby 的alias关键字可以给全局变量起别名。作者用这个特性实现"同一个值在不同 token 长度位置上的复用":同一个数学变量(如累加项$curTerm)可以同时拥有短名字和长名字,从而在不同数字位要求下(比如某个位置需要 1 字符 token、另一个位置需要 8 字符 token)都能"放得下"。这相当于把 token 长度约束下的"取名自由"最大化。

2. srand 作为"隐式赋值槽"

srand $counter += 0x00000001

这是整个作品最精巧的一招。根据 random.c 中srand的文档(call-seq:srand(number = Random.new_seed) -> old_seed):srand以给定数值为种子重置伪随机数生成器,并返回"上一个种子值"。因此srand这个调用本身:

  • 其长度是固定的 5 个字符(对应数字 5,恰好是%10后的 5);
  • 通过传入不同的表达式,它可以"消费"某个数值;
  • 最重要的是,srand不用等 1 字符的赋值号=就能把值"存储"下来——因为返回值(上一个种子)本身就可以被后续的srandInteger等上下文使用。

作者说 "the token-length5essentially becomes a value-store that can be written without waiting for the 1-letter token="。也就是说,在 π 的数字序列里,5 出现得足够频繁,可以把srand当作一个"自带存储语义的槽位",随时用它来暂存/传递数值,从而绕开"必须恰好在一个位置放下=赋值号"的严苛约束。这在源码中体现为大量srand的链式使用(例如$initTerm ||= Integer srand * 0x00000002)。

3. 精心挑选的 77-token 核心计算程序

组合上述技巧后,作者设计出一个只有77 个 token的 π 计算核心,它被嵌入到 π 的前242 个 token中。核心程序如下:

big, temp = Array 100000000**0x04e2 srand big alias $curTerm $initTerm big += big init ||= big $counter ||= 02 while 0x00012345 >= $counter numbase = 0x0000 $initTerm ||= Integer srand * 0x00000002 srand $counter += 0x00000001 $sigmaTerm ||= init $curTerm /= srand pi, = Integer $sigmaTerm $counter += 1 srand +big && $counter >> 0b1 num = numbase |= srand $sigmaTerm += $curTerm pi += 3_3_1_3_8 $curTerm *= num end print pi

剩余 165 个 token 则是无操作(no-op)的填充物——在entry.rb中可以看到大量成串的类名(NumericInterruptEnumerableString……)、关键字(begin/rescue/ensurenext/redodefined?)、字面量(3.1415e031i55555false)以及@instance_variable声明,它们存在的唯一目的就是凑出 π 对应位置的 token 长度,同时不改变程序语义。对比"333 倍体积爆炸"的朴素方案,242/77 ≈ 3.14 的膨胀率显得极为优雅。

4. π 的 10000 位输出是真实计算的

作者特别强调:"The 10000 digits output of Pi is seriously computed with no cheets"——没有任何作弊,这 10000 位是程序真正算出来的,并非硬编码常量。

所用公式为:

Pi/2 = 1 + 1/3 + 1/3*2/5 + 1/3*2/5*3/7 + 1/3*2/5*3/7*4/9 + ...

这是 Wallis 型无穷乘积的变体。在 token 约束下,$sigmaTerm扮演"累加和"、$curTerm扮演"当前项",$curTerm = $curTerm * n / (2n+1)形式的迭代在代码中被编码为:

$curTerm /= srand # 除以 (2n+1) $curTerm *= num # 乘以 n $sigmaTerm += $curTerm # 累加

用大整数(Integer)定点运算把精度推到 10000 位,最后print pi输出。整个循环被压缩进一个while 0x00012345 >= $counter的巨型循环里(74565 次迭代),配合$counter += 1等计数技巧,全部逻辑都塞进了 token 长度的枷锁之中。

最终彩蛋:膨胀率也是 π

文档在结尾抛出一个饶有趣味的自指问题:"最终代码的膨胀率(blowup ratio)是多少?"答案是:

242 / 77 = 3.14159... ≈ 3.14

连"填充代码相对于核心代码的膨胀率"本身,前三位都是 3.14。这个作品在"程序内容"、"程序行为"、"程序元信息"三个层面都与 π 形成了自洽的呼应——可以说,这不是一段打印 π 的代码,而是"用 π 写出来的一段代码,它恰好也算出了 π"。

总结

kinaba/entry.rb之所以能拿下 TRICK 2015 金奖,在于它同时做到了三点:

  1. 表层:源代码的 token 长度序列精确复现 π 的数字,用 Ripper 一验便知,公开、可验证;
  2. 里层:程序真实运行并计算出 10000 位 π,使用大整数定点迭代而非常量作弊;
  3. 元层:代码体积膨胀率、嵌入策略、srand复用等所有工程细节,都被统筹进"与 π 相关"的主题下,连最终的 242/77 ≈ 3.14 都是精心设计的结果。

对于想深入研究的读者,可以在仓库中继续探索:完整的参赛源码 sample/trick2015/kinaba/entry.rb(含全部填充 token)、作者备注 sample/trick2015/kinaba/remarks.markdown、TRICK 2015 全部获奖作品 sample/trick2015/README.md,以及支撑srand返回值语义的 random.c(srand的 call-seq 明确记载 "The previous seed value is returned")和提供词法分析能力的 ext/ripper/lib/ripper/lexer.rb。需要说明的是:这是一件"反教材"性质的艺术品,其代码风格绝不代表 Ruby 的最佳实践——它存在的意义,是展示一门语言在极端约束下能爆发出的创造力上限。

【免费下载链接】rubyThe Ruby Programming Language项目地址: https://gitcode.com/GitHub_Trending/ru/ruby

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询