Esolang——1.brainfuck初探
在前言中我们提到了一种语言,brainfuck。接下来的几篇里,我们将详细介绍这种语言,并使用其进行结构化编程。
背景
1993年,一个叫做FALSE的Esolang出现了,它是图灵完备的,而且最初的编译器只有1024个字节。这种语言影响了后来的许多Esolang,比如Befunge和我们今天要谈到的brainfuck。
同样是在1993年,Urban Müller发现了这个编译器极小的语言后,想要更上一层楼,发明一个使用更小体积编译器的语言。于是一个只需要240字节的编译器横空出世。后来Brain Raiter更是写出了一个只需要171字节的编译器。[3]
这种语言是图灵完备的。事实上,现在证明图灵完备的方法之一就是写出一个brainfuck解释器(就是不知道这种方法学不学术……)。
Müller将其命名为brainfuck。然而,由于众所周知的原因,该语言的名称常被隐去,取而代之的是brainf*ck、bf、brainoof等等。如果要中文翻译的话,脑操真的挺适合的,不管是直译还是意译。
Brainfuck,官方建议是除了句子开头以外全部用小写,不过大写问题也不大。
语言概述
Brainfuck基于这样一台机器。它具有一列初始化为0的数组(数组长度最初要求是30,000,但这个标准不是必要的)和一个初始指向第一个元素的指针。
该语言总共只有8个命令><+-.,[]
以下是每一个命令的含义以及对应的C语句(p是一个char*):

解释器
fatiherikli写了一个解释器,拥有27个上限为255的存储单元,运行代码时可以看到当前运行命令和各个单元的数值。
网址:http://fatiherikli.github.io/brainfuck-visualizer
这里还有一个解释器,支持负的数组下标,数组长度也不只局限于27,但是没有过程显示。
网址:https://tio.run/#brainfuck#
还能搜到其他一些解释器。而且由于brainfuck本身不难,写一个解释器也不是什么难事。在学Brainfuck的时候,开个解释器很有必要。与之同样必要的还有ASCII码表,除非你已经背下来了。
输出Hello, World!
当拿到一种新的语言时,作为惯例,第一个程序永远是打印一个Helloworld。不过在那之前,我们先了解一些知识。
首先,如何用BF换行?
需要注意的是,BF标准规定换行符使用'\n'(10)而非'\r'(13),所以现在我们需要输出一个10。我们使用+和.两个命令
++++++++++.
这样我们就成功换行。
但接着问题就来了,如果我要打印一个'A'(65)怎么办?写65个+?估计这样的话键盘磨损很快吧(当然如果你用程序输出程序当我没说)
这种时候,我们就需要使用乘法了。65=8*8+1,而乘法可以使用循环写,我们再引入其他几个命令。
++++++++[>++++++++<-]>+.
看这个程序,其先将当前位置加8,然后循环运行右移加8左移减1直到左侧单元为0,然后右移加1,就获得了65。这实际上就是将8加8次然后加1。用类似的方法可以大大压缩程序大小。
有了这些知识储备,我们就可以写一个输出"Hello, World!"的程序了
++++++++[>+++++++++<-]>.>>++++++++++[<++++++++++>-]<+.+++++++..+++.>>++++[<+++++++++++>-]<.------------.<<<+++[>+++++<-]>.>.+++.------.--------.>+.(147)
括号里就是整个程序的长度,如你所见,这是很简单的。
但是,brainfuck之所以被称为脑操,不仅是因为它的直译,还因为这玩意儿就像智力游戏一样,极度耗脑子。用BF的人们以压缩程序长度为乐,而有时候极度困难的。比如说,下面就是一个更短的输出"Hello, World"的bf程序。
++++++++[>++++[>++>+++>+++>+>+<<<<<-]>+>+>->+[<]<-]>>.>---.+++++++..+++.>>++++.>.<<-.<.+++.------.--------.>>>+.(112)
这程序比起上面我写的看起来就有点复杂了。没事,咱们一点一点分析
>++++[>++>+++>+++>+>+<<<<<-]
这一段话结束后从第二个元素开始会是0 8 12 12 4 4,指针在第二个元素上
>+>+>->+
这段话结束后是0 9 13 11 5 4
[<]
向左移到第一个为0的元素,即第二个元素
++++++++[>++++[>++>+++>+++>+>+<<<<<-]>+>+>->+[<]<-]
将8乘上上述数列,结果从第一个元素开始为0 0 72 104 88 40 32
>>.>---.+++++++..+++.>>++++.>.<<-.<.+++.------.--------.>>>+.
然后分别加上或减去偏移量输出。
实际上,如果我们放宽限制,或者考虑上BF的其他特性,我们还能得到更短的程序。
比如,如果元素的值没有范围限制且允许负数,那么我们可以写出如下程序[4]
(>>>>>>>>>>>>>>>>)+[+[<<<+>>>>]+<-<-<<<+<++]<<.<++.<++..+++.<<++.<---.>>.>.+++.------.>-.>>--.(76(92))
--->->->>+>+>>+[++++[>+++[>++++>-->+++<<<-]<-]<+++]>>>.>-->-.>..+>++++>+++.+>-->[>-.<<](87)
第一个程序使用了负的数组下标,长度旁边括号里的数字是加上最开始移位后程序的长度。
再比如,如果我们规定超出0~255范围的数字需要取模255(这实际上是大部分解释器规定的),我们还有如下三个程序。[4][5]
(>>>>)--<-<<+[+[<+>--->->->-<<<]>]<<--.<++++++.<<-..<<.+.>>.>>.<<<.+++.>>.>>-.<<<+.(78(82))
(>)-[++[<++>->+++>++<<]---->+]<<<<.<<<<-.<..<<+.<<<<.>>.>>>-.<.+++.>>.>-.<<<<<+.(78(79))
(>>>>>)+[-->-[>>+>-----<<]<--<---]>-.>>>+.>>..+++[.>]<<<<.+++.------.<<-.>>>>+.(72(77))
不过上面两个程序实在是运行时间太久了,所需的数组长度也非常长。与上面两个相比,最底下那个简直可以称之为优美。它是由KSab用C++程序暴力破解出来的。这个可以在上面提到的fatiherikli的解释器中运行,强烈建议输进去欣赏一下。
所以BF真的挺废脑子的,你永远也不知道一个更简单的程序可能会长啥样。这可能就是BF的魅力之一吧。

参考:
[1]brainfuck - Esolang https://esolangs.org/wiki/Brainfuck
[2]The Brainfuck Programming Language http://www.muppetlabs.com/~breadbox/bf/
[3]Basics of Brainfuck https://gist.github.com/roachhd/dce54bec8ba55fb17d3a#file-readme-md
[4]code golf - "Hello, World!" - Code Golf Stack Exchange https://codegolf.stackexchange.com/questions/55422/hello-world/163590#163590
[5]code golf - "Hello, World!" - Code Golf Stack Exchange https://codegolf.stackexchange.com/questions/55422/hello-world/68494#68494
[6]Github:ksabry/bfbrute https://github.com/ksabry/bfbrute