那些奇奇怪怪的编程语言

原发在十五言。现在十五言和果壳网一样,死了。


世界上的编程语言,按照其应用领域,可以粗略地分成三类。

有的语言是多面手,在很多不同的领域都能派上用场。这类编程语言叫 general-purpose language,简称 GPL。大家学过的编程语言很多都属于这一类,比如说 C,Java, Python。

有的语言专注于某一特定的领域,甚至只能用在特定的软件中。这类编程语言叫 domain-specific language,简称 DSL。典型的例子如 Game Maker Language,只用在一个叫 Game Maker 的游戏开发软件中。

有的语言则完全没什么卵用。它们设计出来根本不是为了实用的目的,而是为了搞笑,为了玩梗,为了开脑洞,为了证明某个概念,为了测试语言设计的界限,或者纯粹是为了让你没法好好编程……这些语言往往怪里怪气的,一看就不是正经的编程语言。

这就是 esoteric programming language。虽说也可以简称 EPL,但一般人不这样叫,而是取 esoteric 的前两个音节和 language 的第一个音节,叫它 esolang

Esoteric programming language 这个词有点不好翻译。中文维基百科把它译成“深奥的编程语言”——这个翻译让总我觉得有点深奥。知乎的@涛吴 则把它译成“蛋疼编程语言”——蛋疼一词与 esoteric 差别有点大,但用在 esolang 身上还挺贴切。

程序员在正经的工作中当然不会用到 esolang。Esolang 的使用者和创作者主要是一个由爱好者组成的小圈子,圈子中有程序员,有计算机科学家,有业余学习编程的人,也有像我这样基本不会编程的人。他们活跃在互联网的各个角落,还建起了一个叫 Esolang 的维基网站。

说了这么多,举个例子。

Brainfuck

脑……操?

没错,这种 esolang 就叫 brainfuck

看名字就知道不是什么正经的编程语言。

名字虽然不正经,这却是最著名的一种 esolang。很多地方都可以看到它的身影,比如说 Stack Overflow 的404页面:

Brainfuck 的语法非常简单,只有八条指令。除了指令之外,只有一个由很多个存储单元组成的数组(可以想像成一条有无数个格子的纸带),一个指向数组的指针。开始的时候,数组的每个存储单元都被初始化为0,指针指着数组的第一个存储单元。八条指令对应于数组和指针的八个操作:

>	指针向右移动一位
< 指针向左移动一位
+ 指针指向的存储单元加一
- 指针指向的存储单元减一
. 将指针指向的存储单元的内容作为字符输出
, 输入一个字符并保存到指针指向的存储单元
[ 如果指针指向的存储单元为零,向后跳转到对应的 ] 指令处
] 如果指针指向的存储单元不为零,向前跳转到对应的 [ 指令处

这里的方括号其实就相当于 C 语言里的 while 循环。

除了这八条指令以外的所有字符都会被忽略。你可以把它们当成注释。

按照传统,学习编程时的第一个范例程序往往是输出字符串Hello, World!。我们就用 brainfuck 来写一个 Hello World 程序吧。

首先,字母H的 ASCII 码是72,而存储单元的初始值为0。于是我们需要72个+,然后用.输出:

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.

然后,e的 ASCII 码是101,比72多了29,于是我们加上29个+,再用.输出。后面的11个字符也同样处理。最终写出来的程序是:

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.
+++++++++++++++++++++++++++++.
+++++++. .
+++.
-------------------------------------------------------------------.
------------.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++.
++++++++++++++++++++++++.
+++.
------.
--------.
-------------------------------------------------------------------.

长得有点离谱。不过我们只用了一个存储单元,没有移动过指针,也没有用上循环。用上一些技巧之后,代码可以缩短很多,比如说下面这个 primo 写的 Hello World 程序只有87个字符:

--->->->>+>+>>+[++++[>+++[>++++>-->+++<<<-]<-]<+++]>>>.>-->-.>..+>++++>+++.+>-->[>-.<<]

感兴趣的读者可以在这个 brainfuck 在线解释器上试试这些代码。

可以看出,brainfuck 语言的可读性非常糟糕,一个 Hello World 程序就已经完全让人看不懂了。

这样的语言有什么用呢?

文章一开头我就说过了:没什么卵用。没人会用它来写实用的程序。

不过总是能找到一点用处的吧?

一个用处是当成智力题。用 brainfuck 写程序相当考验智力,稍复杂一点的程序写起来就有一种脑子被操的感觉。

另一个用处是用来拼成字符画。由于每个指令只有一个字符,而且可以任意插入别的字符,例如空格和换行,因此很容易把 brainfuck 程序打扮成字符画。就像这样:

第三种用处则关系到 brainfuck 语言的一个重要特点:它是图灵完备的。

极简主义与 Turing Tarpit

图灵完备不稀奇。我们熟悉的编程语言,绝大部分都是图灵完备的。
神奇的地方在于,brainfuck 仅仅用了八条指令,就实现了一个图灵完备的模型。它是一种极简主义的编程语言。

从这点来说,这种语言虽然名字难听,却有着独特的美学价值。

比美学价值更重要的是,它实现起来非常简单。因此,要证明一种编程语言是图灵完备的,我们只需要用它实现一个 brainfuck,或者证明它的一个子集与 brainfuck 等价。很多 esolang 的图灵完备性就是通过 brainfuck 来证明的。

有不少 esolang 以极简主义为自己追求的目标, brainfuck 只是其中的一种。这些语言有些是图灵完备的,有些不是。其中图灵完备的那些称为 Turing tarpit ——中文维基百科把这个词译作“图灵焦油坑”。

提出 Turing tarpit 这个词的是美国计算机科学家,首届图灵奖得主 Alan Perlis。他在《Epigrams on Programming》一书中提到:

Beware of the Turing tar-pit in which everything is possible but nothing of interest is easy.

和别的 Turing tarpit——比如说,只有一条指令的单一指令计算机,基于组合子逻辑的 JotIota,基于字符串重写的 Thue——比起来,brainfuck 有多达八条指令,算是比较复杂的一种,离极简主义的要求还是有一点距离。
不过,brainfuck 还可以简化。

首先,八条指令的操作对象是一个有很多个存储单元的数组。Brainfuck 并没有规定每个存储单元有多大,主流的 brainfuck 实现大多把它定为一个字节(byte)。我们也可以把它改成一个比特(bit);也就是说,每个存储单元只有0和1两种状态。这样+-就没有区别了,我们可以把这两个指令合并,写成@

然后,我们可以用}来表示>@;也就是指针向右移动一格,然后加一。注意向左移动一个和向右移动一格可以抵消,两次加一也可以抵消。由此容易看出,@就相当于<}>相当于}<}。于是我们可以把@换成<}>换成}<},这样就只剩下}<[].,这六条指令了。

六条指令还是比较多。输入输出和循环结构还可以进一步简化,甚至简化到只有三条指令。不过简化的过程比较复杂,我就不介绍了,感兴趣的读者可以看这里

不过,经过这一步步的简化,写出有意义的程序也变得越来越难。

当然,为了实现极简主义的理想,牺牲一点易用性是值得的。

何况,有的 esolang 本来就是为了难用而设计的。

地狱级的编程语言

我在文章开头就说过,某些 esolang 的设计目的,就“纯粹是为了让你没法好好编程”。

这方面最典型的例子是 Malbolge

Malbolge 是 Ben Olmstead 发明于1998年的一种编程语言。Malbolge 这个词来自但丁《神曲・地狱篇》中的地狱第八层——Malebolge。

Malbolge 编程的难度确实称得上是地狱级。它把程序存储在一个三进制虚拟机上,运行过程中会不断地修改自身;同一个字符在程序的不同位置代表不同的指令,而有效的指令总共只有8条;每一条指令在执行之后会被“加密”,变成另一个字符……

事实上,Ben Olmstead 本人也没有写出过一个完整的 Malbolge 程序。第一个 Malbolge 的 Hello World 程序是通过一个 LISP 程序用束搜索算法搜出来的。

这个用 LISP 找出来的 Hello World 程序长这样:

(=<`$9]7<5YXz7wT.3,+O/o'K%$H"'~D|#z@b=`{^Lx8%$Xmrkpohm-kNi;gsedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543s+O<oLm

这不是一个完美的 Hello World 程序,它输出的是“HEllO WORld”,连大小写都做不到统一。

看来这种语言真的是无解了。

但就在几年之后,Malbolge 被一位叫做 Lou Scheffer 的密码学爱好者“破解”了。

Lou Scheffer 意识到,看待 Malbolge 的正确方法是把它看作一套密码系统,而非一种编程语言。于是,他用密码学的方法对 Malbolge 进行了分析,找到了它的“弱点”,写出了几个 Malbolge 程序,还提出了一套写 Malbolge 程序的策略。有了这套策略,用 Malbolge 语言写程序依然十分困难,但已经不再是不可能。

于是,我们写 Hello World 程序的时候不必再用 LISP 来搜索。这是 Jacob 写的一个能完整输出“Hello, World!”的程序

('&%:9]!~}|z2Vxwv-,POqponl$Hjihf|B@@>,=<M:9&7Y#VV2TSn.Oe*c;(I&%$#"mCBA?zxxv*Pb8`qo42mZF.{Iy*@dD'<;_?!\}}|z2VxSSQ

当然,我还是读不懂。

无论是追求极简的 Turing tarpits,还是追求极难的 Malbolge,都可以归结为测试语言设计的界限。正如 Ben Olmstead 在某次采访中所说的:

… pushing the boundaries of programming… but not in useful directions.

有的语言则走得更远,突破了维度的界限。

二维的编程语言

无论是大部分日常的编程语言,还是前面介绍的 brainfuck 和 Malbolge,代码都是一维的。即使有换行和缩进的要求,读代码的时候还是从前往后按顺序阅读。

你有没有想过,有的语言可以把代码铺在一个二维的平面上,用箭头或别的符号来指引阅读和执行的方向,读代码就像走迷宫?

比如说 Befunge

Befunge 是一个基于堆栈的编程语言,大部分指令都是堆栈操作:往堆栈里压入一个东西,从堆栈里弹出一个东西,交换堆栈顶部的两个东西……Befunge 中也有加减乘除、模、逻辑非、大于等常见的运算,只不过要理解为堆栈操作。比如说,加法+表示从堆栈里弹出两个数,把它们加起来,然后把结果压回堆栈。

基于堆栈的编程语言不常见,但也算不上离奇。有些实用的编程语言,比如说 PostScript,Forth,Factor,也是基于堆栈的,只不过 Befunge 里每一条指令都只用一个字符来表示。

除了堆栈操作的指令之外,Befunge 中还有一些指示方向的指令。比如说,^v<>分别表示上下左右四个方向的箭头;问号?表示随机的一个方向;|_这两个符号表示条件选择,比如说_表示从堆栈里弹出一个值,如果它等于0就转向右边,否则转向左边。此外,#表示跳过一个指令,@表示结束程序。

这些指示方向的指令规定了代码执行的顺序:开始的时候,也是从左上角开始向右走,但并不理会换行;一旦遇到指示方向的指令,它就会转向这些指令所指的方向,沿着这个方向走下去。

于是,在 Befunge 里,你看不到别的语言中的循环语句;只要用箭头画出一个回路,就可以实现循环。

比如说,这是一个死循环:

>v
^<

再比如说 Matrix67 的博客里的这个 Hello World 程序:

                 v
>v"Hello world!"0<
,:
^_25*,@

这个程序该怎么读?

首先,从第一行的最左边开始向右走,直到碰到最右边的v。顺着v的方向向下走,遇到<,转向左边。遇到0,把数字0压入堆栈。

然后我们看到了熟悉的"Hello world!"。但这时程序运行的方向是从右往左,所以这个字符串其实是"!dlrow olleH"。为什么要倒过来?因为 Befunge 处理字符串的方式是把它拆成一个个字符,按顺序压入堆栈。因此,把"!dlrow olleH"压入堆栈的时候,!在最下面,H在最上面。输出的时候,就可以先输出H,最后输出!

"Hello world!"的后面是一个向下的箭头。顺着箭头,程序进入了一个由箭头围成的回路:

>v
,:
^_

进入循环之后遇到的第一个非箭头的指令是:,它的意思是把堆栈最顶上的东西复制一份。需要复制是因为在条件选择_中要用掉一份。然后,如果堆栈顶上不是0,则向左拐,再向上,遇到,指令,输出一个字符,向右走回到v处;如果堆栈顶上是0,则在_处直接向右拐退出循环。"Hello World!"这几个字符都不等于0,于是程序把它们按顺序一个个输了出来。

出了循环向右走,是数字2和数字5——Befunge 里的数字要分开一个个读——和一个乘号。2乘5等于10,对应的是换行的 ASCII 码,于是后面的一个,输出换行。最后@结束程序。

Befunge 不是唯一一个二维的 esolang。Esolang 维基的 Two-dimensional languages 分类里列出了一百多种二维编程语言,其中有 Befunge 的模仿者,也有一些脑洞特别大的作品,比如说 Piet

我也曾写过一个 Piet 程序:

没错,这幅图片就是一个 Piet 程序。这种语言的设计目标就是让程序看起来像抽象画。Piet 这个名字正是来自荷兰风格派画家 Piet Mondrian。

更多的脑洞

Befunge 把程序写到二维,Piet 把程序写成图片,都算得上是脑洞之作。

日常的编程语言往往被看作是工具。无论语言本身设计得多精美,多神奇,我们更看重的往往还是写出来的程序;语言本身的精美和神奇之处也往往只是为了写出更好的程序,或者更好地写出程序。

但很多 esolang 并非如此。这些语言并不适合于写程序,令人赞叹的只是语言本身。与其把它们看作编程的工具,不如把它们看成是恰好能用来编程的艺术品。

既然是艺术品,当然可以天马行空,脑洞大开。有怎样的脑洞,就有怎样的编程语言。

下面列举了一些我比较喜欢的作品,限于篇幅就不详细介绍了。

MarioLANG

把程序写成超级马里奥关卡的样子。设计者是 Wh1teWolf

++++:       >       >  +:+:+:+:+:+:+:::::
====+ >^=== """=================
+:-):(:^= = !
========= = #
= ! .+.,:-<
=### ======"

Starry

原悠在《Rubyで作る奇妙なプログラミング言語》一书中设计的编程语言。代码全是星星,读起来有一种仰望星空的感觉。

            +               +  *       +    
* + . + + * +
* * + . + * + . + .
+ * + . + +
* + * * + . + *
+ . + + * + *
* + . + * + . + *
+ . + * + . + * + .
+ + * + *
* + .

Parenthesis Hell

Qpliu 设计的一种 LISP 方言,继承了 LISP 最重要的特点:括号。

((())(((())()((())())(()())((())())(((())())(((())())(()())((())())(())(()))())(())))((())())(()())((())())(((())())(((())())(()())((())())(())(()))())(()))

Unreadable

完全不可读的编程语言,代码中只允许出现两种字符:单引号和双引号。作者是 TehZ

'"'""'""'""'"'"'""""""'""'"""'""'""'""'""'""'""'""'"'""'""""""'""'""'""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""'""""""'""'""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'"'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""""""'""""""""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""""""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'"""'"'"""""""'""""""""'"""'"'"""""""'"""'"'"""""""'""'""'"""'"'""'""'""'"'""'""'""'"""""""'""'"""'"'"""""""'""'"""'"'"""""""'""'""'""'"""'"'""'"""""""'"""

Quipu

古印加人有一套结绳记事的方法,叫做奇普。这种语言就是基于奇普而设计,作者是 Vladimir Kostyukov

"0  1  2  3  4  5  6  7  8"

\/ 1& 3& 4& 3& 7& 3& 1& 7&
-- [] [] [] [] [] ^^ []
0& 1& 2& 6& /\ -- 7&
[] -- [] == 7& 1& >>
-- 7& ++ ', [] >> '.
7& [] /\ 1& /\
[] ** ' >>
** 1& /\
0& ++
[]
++
8&
==

Hexagony

可能是第一种六边形编程语言。不仅代码要排成六边形,它模拟的存储布局也是一个六边形网格。作者是 Martin Büttner

    4 \ / " 1
" 0 . " . .
. 0 . . 0 . =
. \ 0 0 0 / + .
} = \ > - " < > &
. + & . / < . _
} ' . . ) _ !
\ + { = / @
{ { } = +

Rail

把代码铺成铁路。作者是 Jonathon Duerig

$ 'main' (--):
\
| /---------\
| | |
| \ /-io-/
\---e-<
\-#

Polynomial

这是 Maedhros777 设计的一种语言。一个程序就是一个多项式,指令隐藏在多项式的零点当中。

f(x) = x^10 - 4827056x^9 + 1192223600x^8 - 8577438158x^7 + 958436165464x^6 - 4037071023854x^5 + 141614997956730x^4 - 365830453724082x^3 + 5225367261446055x^2 - 9213984708801250x + 21911510628393750

Velato

程序既然能写成图片,当然也能写成音乐。Velato 是一种以 MIDI 音乐为源代码的编程语言,作者是 Rottytooth

除此之外,还有一些更加优秀的作品,出自 Danger Mouse(Piet 的作者)、Timwi 这样的 esolang 大师之手。如果这一系列没有坑掉的话,我会在以后专门用一两篇文章介绍他们的作品。