Quine Relay
今天上网时看到有人分享了这个项目:quine-relay,点进去看是这样一个程序:一开始是一段Ruby代码,运行后生成Scala代码,后者运行后生成Scheme代码,如此执行,依次生成了Bash、Smalltalk、Tcl等代码,最后又回到最初的Ruby代码。这类程序有个名字,叫ouroborus programs。Ouroborus即衔尾蛇,是一条正在吞食自己尾巴的蛇,在这里刚好表示循环的概念。
分析
项目的有趣之处在于两点:塑型,把代码组成了特定图案;一生二,二生三,三生一,形成循环往复的衔尾蛇效果。
塑型
先谈塑型。
初始的Ruby程序位于项目根目录下的QR.rb
,代码自身组织成衔尾蛇和六芒星的造型,这个看上去相当不容易。在一个所有词法单元只占据一个字符且忽略空白字符的语言(比如Brainfuck)中,把代码组织成任意的图案是很容易的。但是对于Ruby等词法单元并非只占据一个字符的语言,直接将代码排版成特定图案是很困难的。quine-relay
采用了一个技巧,在字符串中编码了一段程序用于生成下一种语言的代码,调用eval
解释这个字符串。这个字符串表示是忽略空白字符的。文件src/QR.rb.gen.rb
末段实现了这一功能:
1 | Tmpl = File.read("uroboros.txt") |
Tmpl
是衔尾蛇图案的模板文件,可以看作一个蒙版,指定了哪些地方填字符,哪些地方为空格或换行,把最终代码塑型成该图案。
衔尾蛇效果
quine-relay
想要实现的逻辑可以表达如下:
1 | Ruby {\orig_ruby -> print (Scala {print (Scheme {print orig_ruby})})} |
其中省去了其他语言的print。Ruby {...}
代表一段Ruby代码,{}
中的部分是程序的内容。根据这段逻辑得到的Ruby代码就是orig_ruby
,并且orig_ruby
被自身引用了。
下面分成两部分来解释实现方式:
- 实现不同语言的print
- 在代码中引用自身代码(
orig_ruby
)
实现不同语言的print
执行一下Ruby程序,再执行依次产生的各语言程序,可以发现它们做的事都是print
某个特定字符串,而print
的字符串自然就是下一个程序的源代码了。各语言如何print
一字符串,不少网页都有收集hello-world的例子,比如:
- http://www.roesler-ac.de/wolfram/hello.htm
- http://en.wikipedia.org/wiki/List_of_Hello_world_program_examples
- rosettacode.org/wiki/Hello_world/Text
- c2.com/cgi/wiki?HelloWorldInManyProgrammingLanguages
src/code-gen.rb
里有大量编程语言输出字符串的模板代码,比如这段OCaml的:
1 | class OCaml < CodeGen |
PREV
之后会被替换为OCaml生成的下一个编程语言的代码,E
则是用于字符串的引号转义,并非看懂需要关注非常重要的点,暂时不用理会。
观察src/QR.rb.gen.rb
的头几行,可以发现作者在试图构建单链表效果:
1 | s = '%(eval$s=%q(#$s))' |
CodeGen::List[0..-2]
就是除去开头结尾的Ruby以外其他各编程语言了,每一个都以需要产生的下一个语言的代码为参数,返回用本语言写成的,返回那段字符串的代码。
在代码中引用自身代码
把Ruby以外的其他编程语言的CodeGen
子类都注释掉,在src
目录下执行rake ../QR.rb
,对QR.rb
抽丝剥茧后可以观察到这段核心代码:
1 | eval$s=%q(eval(%w( |
这段代码是原始的Ruby写成的quine,执行后的标准输出依然为这段代码自身。理解它需要了解Ruby从Perl学习来的语法:general
delimited
input,即%w
、%q
这些符号,以及Array
重载的*
操作符的含义:
先看$s
,它是%q()
构建的字符串,eval
它的效果是再次eval
下面的字符串:
1 | %w(puts(eval(%q(%(eval$s=%q (#$s)))))) * "" |
其中使用了Array#*
的方法,即Array#join
,因此效果是eval
下面的字符串:
1 | puts(eval(%q(%(eval$s=%q(\#$s))))) |
即输出eval$s=%q(
后,输出$s
的值,再跟)
。这段Ruby
quine的逻辑是:
1 | eval$s=%q( |
字符串$s
包含了一段需要eval的代码,代码中包含了
$s`实现了自指,这可以看作是一种fixed-point
combinator:Y combinator,的实现。
定义函数f
,它应用参数t
的效果是打印t
的代码。根据fixed-point
theorem,存在代码n
使得f(n)=n
,也就是说n
会打印自身代码。下面说明这段Ruby代码本质上就是n
的一种构造。
$s
可以看作一个函数:y = \x -> f (x x)
。$s
中的自指以及eval
的解释操作,可以看作把自身作为参数应用给自己:y y
,y
得到参数x
后的效果是把x x
的代码打印出来。而打印x x
需要打印以code形式存在的x
和与之关联的以data形式存在的x
。考虑到参数传递给y
的参数是一个固定值x
即y
,我们不需要写一个通用的能打印任意代码的程序,只需要针对这个特例,实现一个特例。在这里data是$s
,code是被eval
解释的$s
,而所要打印的代码则是eval$s=%q(
、一段任意字符串和)
。
y y
即(\x -> f (x x)) (\x -> f (x x))
,应用一次会打印自身代码(副作用)并返回自身。为了表述方便没有严格考虑evaluation
strategy的影响,这里应该视为使用了call-by-need,但是最外层的y y
应用有一次强制的求值。
另外这段代码的puts
前可以执行任意多不影响标准输出的代码,puts
的内容(括号里面)也可以接一些代码,比如最终QR.rb
里puts
的内容就跟着.gsub(/[HJK^`X]/)
等一长串代码。src/code-gen.rb
里在puts
前添加了PROLOGUE
,用来规整化第一个Ruby程序看到的下一段代码字符串。比如用N
代替换行符,B
代替反斜杠等,而e
、E
、d
、D
、Q
等则是各语言里字符串内特殊字符的转义方式,在CodeGen
各子类的hello-world模板代码里用到了这些转义函数。
其他
另外值得称道的是项目中的一款Whitespace解释器:src/whitespace.rb
,词法符号的提取用了\G
、named
capture、named backreference等高级玩意儿:
1 | RE = /\G(?<n>[01]+){0} |
x
开启free-spacing
mode,正则表达式里的空白符号会被引擎忽略。|
分割的子表达式每个都表示一个词法结构,用named
capture来具体是哪个子表达式匹配成功了。题外话,关于正则表达式,《Mastering
Regular Expressions》、《Regular Expressions Cookbook》都值得一读。