盘点那些奇形怪状的编程语言
世界上的编程语言,按照其应用领域,可以粗略地分成三类。
有的语言是多面手,在很多不同的领域都能派上用场。这类编程语言叫 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 在内的几种不同的语言中运行,输出结果都是404
Brainfuck 的语法非常简单,只有八条指令。除了指令之外,只有一个由很多个存储单元组成的数组(可以想像成一条有无数个格子的纸带),一个指向数组的指针。开始的时候,数组的每个存储单元都被初始化为0,指针指着数组的第一个存储单元。八条指令对应于数组和指针的八个操作:
代码语言:javascript复制> 指针向右移动一位
< 指针向左移动一位
+ 指针指向的存储单元加一
- 指针指向的存储单元减一
. 将指针指向的存储单元的内容作为字符输出
, 输入一个字符并保存到指针指向的存储单元
[ 如果指针指向的存储单元为零,向后跳转到对应的 ] 指令处
] 如果指针指向的存储单元不为零,向前跳转到对应的 [ 指令处这里的方括号其实就相当于 C 语言里的 while 循环。
除了这八条指令以外的所有字符都会被忽略。你可以把它们当成注释。
按照传统,学习编程时的第一个范例程序往往是输出字符串Hello, World!。我们就用 brainfuck 来写一个 Hello World 程序吧。
首先,字母H的 ASCII 码是72,而存储单元的初始值为0。于是我们需要72个+,然后用.输出:
代码语言:javascript复制++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.然后,e的 ASCII 码是101,比72多了29,于是我们加上29个+,再用.输出。后面的11个字符也同样处理。最终写出来的程序是:
代码语言:javascript复制++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++.
+++++++++++++++++++++++++++++.
+++++++. .
+++.
-------------------------------------------------------------------.
------------.
+++++++++++++++++++++++++++++++++++++++++++++++++++++++.
++++++++++++++++++++++++.
+++.
------.
--------.
-------------------------------------------------------------------.长得有点离谱。不过我们只用了一个存储单元,没有移动过指针,也没有用上循环。用上一些技巧之后,代码可以缩短很多,比如说下面这个 primo 写的 Hello World 程序只有87个字符:
代码语言:javascript复制--->->->>+>+>>+[++++[>+++[>++++>-->+++<<<-]<-]<+++]>>>.>-->-.>..+>++++>+++.+>-->[>-.<<]感兴趣的读者可以在这个 brainfuck 在线解释器上试试这些代码。
可以看出,brainfuck 语言的可读性非常糟糕,一个 Hello World 程序就已经完全让人看不懂了。
这样的语言有什么用呢?
文章一开头我就说过了:没什么卵用。没人会用它来写实用的程序。
不过总是能找到一点用处的吧?
一个用处是当成智力题。用 brainfuck 写程序相当考验智力,稍复杂一点的程序写起来就有一种脑子被操的感觉。
另一个用处是用来拼成字符画。由于每个指令只有一个字符,而且可以任意插入别的字符,例如空格和换行,因此很容易把 brainfuck 程序打扮成字符画。就像这样:
输出结果是 Guokr
第三种用处则关系到 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——比如说,只有一条指令的单一指令计算机,基于组合子逻辑的 Jot 和 Iota,基于字符串重写的 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 程序长这样:
代码语言:javascript复制(=<`$9]7<5YXz7wT.3,+O/o'K%$H"'~D|#z@b=`{^Lx8%$Xmrkpohm-kNi;gsedcba`_^]\[ZYXWVUTSRQPONMLKJIHGFEDCBA@?>=<;:9876543s+O 看来这种语言真的是无解了。 但就在几年之后,Malbolge 被一位叫做 Lou Scheffer 的密码学爱好者“破解”了。 Lou Scheffer 意识到,看待 Malbolge 的正确方法是把它看作一套密码系统,而非一种编程语言。于是,他用密码学的方法对 Malbolge 进行了分析,找到了它的“弱点”,写出了几个 Malbolge 程序,还提出了一套写 Malbolge 程序的策略。有了这套策略,用 Malbolge 语言写程序依然十分困难,但已经不再是不可能。 于是,我们写 Hello World 程序的时候不必再用 LISP 来搜索。这是 Jacob 写的一个能完整输出“Hello, World!”的程序: 代码语言:javascript复制('&%:9]!~}|z2Vxwv-,POqponl$Hjihf|B@@>,= 无论是追求极简的 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 里,你看不到别的语言中的循环语句;只要用箭头画出一个回路,就可以实现循环。 比如说,这是一个死循环: 代码语言:javascript复制>v ^<再比如说 Matrix67 的博客里的这个 Hello World 程序: 代码语言:javascript复制 v >v"Hello world!"0< ,: ^_25*,@这个程序该怎么读? 首先,从第一行的最左边开始向右走,直到碰到最右边的v。顺着v的方向向下走,遇到<,转向左边。遇到0,把数字0压入堆栈。 然后我们看到了熟悉的"Hello world!"。但这时程序运行的方向是从右往左,所以这个字符串其实是"!dlrow olleH"。为什么要倒过来?因为 Befunge 处理字符串的方式是把它拆成一个个字符,按顺序压入堆栈。因此,把"!dlrow olleH"压入堆栈的时候,!在最下面,H在最上面。输出的时候,就可以先输出H,最后输出!。 "Hello world!"的后面是一个向下的箭头。顺着箭头,程序进入了一个由箭头围成的回路: 代码语言:javascript复制>v ,: ^_进入循环之后遇到的第一个非箭头的指令是:,它的意思是把堆栈最顶上的东西复制一份。需要复制是因为在条件选择_中要用掉一份。然后,如果堆栈顶上不是0,则向左拐,再向上,遇到,指令,输出一个字符,向右走回到v处;如果堆栈顶上是0,则在_处直接向右拐退出循环。"Hello World!"这几个字符都不等于0,于是程序把它们按顺序一个个输了出来。 出了循环向右走,是数字2和数字5——Befunge 里的数字要分开一个个读——和一个乘号。2乘5等于10,对应的是换行的 ASCII 码,于是后面的一个,输出换行。最后@结束程序。 Befunge 不是唯一一个二维的 esolang。Esolang 维基的 Two-dimensional languages 分类里列出了一百多种二维编程语言,其中有 Befunge 的模仿者,也有一些脑洞特别大的作品,比如说 Piet。 我也曾写过一个 Piet 程序: Project Euler 第五题的解答 没错,这幅图片就是一个 Piet 程序。这种语言的设计目标就是让程序看起来像抽象画。Piet 这个名字正是来自荷兰风格派画家 Piet Mondrian。 更多的脑洞Befunge 把程序写到二维,Piet 把程序写成图片,都算得上是脑洞之作。 日常的编程语言往往被看作是工具。无论语言本身设计得多精美,多神奇,我们更看重的往往还是写出来的程序;语言本身的精美和神奇之处也往往只是为了写出更好的程序,或者更好地写出程序。 但很多 esolang 并非如此。这些语言并不适合于写程序,令人赞叹的只是语言本身。与其把它们看作编程的工具,不如把它们看成是恰好能用来编程的艺术品。 既然是艺术品,当然可以天马行空,脑洞大开。有怎样的脑洞,就有怎样的编程语言。 下面列举了一些我比较喜欢的作品,限于篇幅就不详细介绍了。 MarioLANG把程序写成超级马里奥关卡的样子。设计者是 Wh1teWolf。 代码语言:javascript复制++++: > > +:+:+:+:+:+:+::::: ====+ >^=== """================= +:-):(:^= = ! ========= = # = ! .+.,:-< =### ======"Starry原悠在《Rubyで作る奇妙なプログラミング言語》一书中设计的编程语言。代码全是星星,读起来有一种仰望星空的感觉。 代码语言:javascript复制 + + * + * + . + + * + * * + . + * + . + . + * + . + + * + * * + . + * + . + + * + * * + . + * + . + * + . + * + . + * + . + + * + * * + .Parenthesis HellQpliu 设计的一种 LISP 方言,继承了 LISP 最重要的特点:括号。 代码语言:javascript复制((())(((())()((())())(()())((())())(((())())(((())())(()())((())())(())(()))())(())))((())())(()())((())())(((())())(((())())(()())((())())(())(()))())(()))Unreadable完全不可读的编程语言,代码中只允许出现两种字符:单引号和双引号。作者是 TehZ。 代码语言:javascript复制'"'""'""'""'"'"'""""""'""'"""'""'""'""'""'""'""'""'"'""'""""""'""'""'""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""'""""""'""'""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'"'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""""""'""""""""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""""""'"""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'""'"""'"'"""""""'""""""""'"""'"'"""""""'"""'"'"""""""'""'""'"""'"'""'""'""'"'""'""'""'"""""""'""'"""'"'"""""""'""'"""'"'"""""""'""'""'""'"""'"'""'"""""""'"""Quipu古印加人有一套结绳记事的方法,叫做奇普。这种语言就是基于奇普而设计,作者是 Vladimir Kostyukov。 代码语言:javascript复制"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。 代码语言:javascript复制 4 \ / " 1 " 0 . " . . . 0 . . 0 . = . \ 0 0 0 / + . } = \ > - " < > & . + & . / < . _ } ' . . ) _ ! \ + { = / @ { { } = +Rail把代码铺成铁路。作者是 Jonathon Duerig。 代码语言:javascript复制$ 'main' (--): \ | /---------\ | | | | \ /-io-/ \---e-< \-#Polynomial这是 Maedhros777 设计的一种语言。一个程序就是一个多项式,指令隐藏在多项式的零点当中。 代码语言:javascript复制f(x) = x^10 - 4827056x^9 + 1192223600x^8 - 8577438158x^7 + 958436165464x^6 - 4037071023854x^5 + 141614997956730x^4 - 365830453724082x^3 + 5225367261446055x^2 - 9213984708801250x + 21911510628393750Velato程序既然能写成图片,当然也能写成音乐。Velato 是一种以 MIDI 音乐为源代码的编程语言,作者是 Rottytooth。 这只是程序对应的乐谱,真正的程序是一个 MIDI 文件 除此之外,还有一些更加优秀的作品,出自 Danger Mouse(Piet 的作者)、Timwi 这样的 esolang 大师之手。如果这一系列没有坑掉的话,我会在以后专门用一两篇文章介绍他们的作品。