手动实现一门图灵完备的编程语言——Brainfuck

Posted 麒思妙想

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了手动实现一门图灵完备的编程语言——Brainfuck相关的知识,希望对你有一定的参考价值。

 

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

如果你看了上面这串字符,并知道它代表什么,那么相信你在你读完这篇文章后,你会理解,它为什么代表字符串 hello world! 并且能够完成自己的解析器代码。

首先,它是一门编程语言叫 brianfuck , 然后,它是一门图灵完备的编程语言。

那么什么是图灵完备呢?我们还需要先从图灵机说起。 

图灵机

图灵机,又称图灵计算、图灵计算机,是由数学家艾伦·麦席森·图灵(1912~1954)在1936年发表的 “On Computable Numbers, with an Application to the Entscheidungsproblem”(《论可计算数及其在判定性问题上的应用》)中提出的数学模型。既然是数学模型,它就并非一个实体概念,而是架空的一个想法。在文章中图灵描述了它是什么,并且证明了,只要图灵机可以被实现,就可以用来解决任何可计算问题

图灵机的结构包括以下几个部分:

  • 一条无限长的纸带(tape),纸带被分成一个个相邻的格子(square),每个格子都可以写上至多一个字符(symbol)。

  • 一个字符表(alphabet),即字符的集合,它包含纸带上可能出现的所有字符。其中包含一个特殊的空白字符(blank),意思是此格子没有任何字符。

  • 一个读写头(head),可理解为指向其中一个格子的指针。它可以读取/擦除/写入当前格子的内容,此外也可以每次向左/右移动一个格子。

  • 一个状态寄存器(state register),它追踪着每一步运算过程中,整个机器所处的状态(运行/终止)。当这个状态从运行变为终止,则运算结束,机器停机并交回控制权。如果你了解有限状态机,它便对应着有限状态机里的状态。

  • 一个有限的指令集(instructions table),它记录着读写头在特定情况下应该执行的行为。可以想象读写头随身有一本操作指南,里面记录着很多条类似于“当你身处编号53的格子并看到其内容为0时,擦除,改写为1,并向右移一格。此外,令下一状态为运行。”这样的命令。其实某种意义上,这个指令集就对应着程序员所写下的程序了。 

    在计算开始前,纸带可以是完全空白,也可以在某些格子里预先就有写上部分字符作为输入。运算开始时,读写头从某一位置开始,严格按照此刻的配置(configuration),即:

     

  • 当前所处位置

  • 当前格子内容来一步步的对照着指令集去进行操作,直到状态变为停止,运算结束。而后纸带上留下的信息,即字符的序列(比如类似“…011001…”)便作为输出,由人来解码为自然语言。要重申一下,以上只是图灵机模型的内容,而非具体的实现。所谓的纸带和读写头都只是图灵提出的抽象概念。为便于理解 打一个比方。算盘虽然不是图灵机(因为它没有无限长的纸带,即无限的存储空间),但它的行为与图灵机一致。每一串算珠都是纸带上的一格,一串算珠上展示的数字便记录着当前格中的字符(可以是空白,可以是 12345 )。人类的手即是读写头,可以更改每串算珠的状态。算盘的运行遵循人脑中的算法,当算法结束,算盘停机。

(以上引用自:https://www.zhihu.com/question/20115374)

图灵机能干什么

简单的说图灵机,只能解决计算相关问题,举一些简单的例子

  1. 大爆炸的起点

  2. 弹道的路线

不能解决的问题,比如

  1. 今晚看什么电影?

  2. 神是否存在?

这里说,图灵机可以解决计算问题,但是不是计算问题都可以解决,这里就不做展开说明了。

模仿游戏

说到图灵,那就顺便再推荐一手《模仿游戏》。

《模仿游戏》(The Imitation Game),是由莫腾·泰杜姆执导,本尼迪克特·康伯巴奇、凯拉·奈特莉等主演的传记电影。

影片改编自安德鲁·霍奇斯编著的传记《艾伦·图灵传》,讲述了“计算机科学之父”艾伦·图灵的传奇人生,故事主要聚焦于图灵协助盟军破译德国密码系统“英格玛”,从而扭转二战战局的经历 。

该片获得第87届奥斯卡金像奖最佳改编剧本奖,以及包括最佳影片、最佳导演、最佳男主角、最佳女配角在内的7项提名。于2015年7月21日在中国大陆上映。

不得不说图灵的结局是悲惨的,碍于当时的社会风气,我们早早的失去了这么一位天才... ...

图灵完备

!!Formal definitions 

In computability theory, several closely related terms are used to describe the computational power of a computational system (such as an abstract machine or programming language): 

!Turing completeness 
A computational system that can compute every Turing-computable function is called Turing-complete (or Turing-powerful). Alternatively, such a system is one that can simulate a universal Turing machine. 

!Turing equivalence 
A Turing-complete system is called Turing-equivalent if every function it can compute is also Turing-computable; i.e., it computes precisely the same class of functions as do Turing machines. Alternatively, a Turing-equivalent system is one that can simulate, and be simulated by, a universal Turing machine. (All known physically-implementable Turing-complete systems are Turing-equivalent, which adds support to the Church–Turing thesis.[citation needed]) 

!(Computational) universality 
A system is called universal with respect to a class of systems if it can compute every function computable by systems in that class (or can simulate each of those systems). Typically, the term universality is tacitly used with respect to a Turing-complete class of systems. The term "weakly universal" is sometimes used to distinguish a system (e.g. a cellular automaton) whose universality is achieved only by modifying the standard definition of Turing machine so as to include input streams with infinitely many 1s.

只要这些规则可以用来模拟图灵的假想计算机,那么这些规则就被 图灵完备。而图灵完备的系统可以通过数学证明能够访问任何可能的计算或计算机程序。

有一类图灵完备的系统称作lambda calcullas,是图灵的导师Alonzo Church,相信对各位熟悉函数式编程的朋友一定不陌生。

brainfuck

简介

brainfuck 是 Urbak Muller 于1993年发明的语言,这门语言可以说是编程语言界的helloworld,它一个值有8个字符,每个有效字符就是一条命令,它就是一门图灵完备的编程语言,如果能理解他的工作原理,那么对于理解图灵机有很大的帮助。

Brainfuck is fully Turing-complete.

下面看一下它的8个有效字符指令:

字符含义方法
>指针向右移一forward()
<指针向左移一backward()
+当前指针指向的数据带值+1increase()
-当前指针指向的数据带值-1reduce()
.将当前指针指向的数据带值的ASCII码打印print()
,获取键盘输入的字节流,写入当前指针指向的数据带input()
[循环开始,如果当前指针指向的数据带值为 0,则跳到与之匹配的 ]后一条指令whileEntity()
]循环结束,  如果当前指针指向的数据带值不为 0,则跳到与之匹配的 [后一条指令whileEnd()

实现

这里我们使用了一个整形的 ArrayList<Integer> 来模拟数据带,所以指向数据带的指针,可以使用一个 int 变量来模拟,我们使用一个 char[] 数组来存放指令集,还是使用一个 int 变量来做指令执行的指针,他代表执行到指令的具体位置。

/**
* 当前指针
*/
private int currentPointer = 0;

/**
* 模拟指针指向的数据带
* 使用ArrayList数据结构来模拟,实现过程中,需要自己越界问题
*/
private List<Integer> band = new ArrayList<>();

/**
* 存储输入的指令集,以便后续程序调研
* */
private char[] chars;

/**
* 指令指针
* */
private int currentCmdPointer = 0;

+ - , . < < 这几个指令的含义相当容易理解,只需要注意边界即可。

 
 /**
    * 获取键盘输入的字节流,写入当前指针指向的数据带
    * */
   private void input() 
       try 
           this.band.add(this.currentPointer, (int) System.in.read());
       catch (IOException e) 
           e.printStackTrace();
      
  

   /**
    * 将当前指针指向的数据带值的ASCII码打印
    * */
   private void print() 
       System.out.printf("%c",this.band.get(this.currentPointer));
  


   /**
    * 指针向又移一
    * */
   private void forward() 
       this.currentPointer++;
       //处理越界
       if(this.currentPointer >= this.band.size()-1)
           this.band.add(0);
      
  


   /**
    * 指针向左移一
    * */
   private void backward() 
       --this.currentPointer;
       //处理越界
       this.currentPointer = this.currentPointer < 0 ? 0 : this.currentPointer;
  

   /**
    * 当前指针指向的数据带值-1
    * */
   private void reduce() 
       Integer temp = this.band.get(this.currentPointer);
       temp--;
       this.band.set(this.currentPointer,temp);
  

   /**
    * 当前指针指向的数据带值+1
    * */
   private void increase() 
       //规避数据越界问题
       for(int i = this.band.size(); i<=this.currentPointer;i++)
           this.band.add(0);
      
       //获取当前指针值,+1,并写回
       Integer temp = this.band.get(this.currentPointer);
       temp++;
       this.band.set(this.currentPointer,temp);
  

[ ] 两部分的指令含义相对复杂一点,需要理解其含义,[ 相当于进入循环体,在符合条件后,跳过循环,]相当于循环的结尾判断,如果不符合条件,将从头继续执行循环。

/**
    * 循环结束
    * 如果当前指针指向的数据带值不为 0,则跳到与之匹配的 '['后一条指令
    * */
   private void whileEnd() 
       if(this.band.get(this.currentPointer)!=0)
           while(true)
               if(this.chars[--this.currentCmdPointer]=='[')
                   break;
              
          
      
  

   /**
    * 循环开始
    * 如果当前指针指向的数据带值为 0,则跳到与之匹配的 ']'后一条指令
    * */
   private void whileEntity() 
       if(this.band.get(this.currentPointer)==0)
           while(true)
               if(this.chars[this.currentCmdPointer++]==']')
                   break;
              
          
      
  

完整代码:

package name.lijiaqi.brainfuck;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

/**
*
* Brainfuck解析实现
* @author: +7
* */
public class BF 

   public static void main(String[] args) 
       List<String> cmds = new ArrayList<>();
       // 输入输出循环...
       String str = ",[.,]";
       //cmds.add(str);

       //25868716
       str = "+++++[>++++++++++<-]>.+++.+++.--.++.-.------.+++++.";
       cmds.add(str);

       //2
       str = "+++++[>++++++++++<-]>.";
       cmds.add(str);

       //1
       str = ">+++++++[<+++++++>-]<.";
       cmds.add(str);

       //hello world!
       str = ">++++++++[<+++++++++++++>-]<.---.+++++++..+++.>++++++++[<---------->-]<+.>++++++++[<+++++++++++>-]<-.--------.+++.------.--------.>++++++[<----------->-]<-.";
       cmds.add(str);

       //hello sange!
       str = ">++++++++[<+++++++++++++>-]<.---.+++++++..+++.>++++++++[<---------->-]<+.>+++++++++[<+++++++++>-]<++.>+++[<------>-]<.+++++++++++++.-------.--.>++++++[<----------->-]<--.";
       cmds.add(str);

       //hello Shostakovich
       str = "++++++++++[>+>+++>+++++++>++++++++++<<<<-]>>>>++++.---.+++++++..+++.<<++.>+++++++++++++.>-------.+++++++.++++.+.<++++++++++++++.>---------.++++.+++++++.<++++++++.------.+++++.";
       cmds.add(str);

       cmds.stream().forEach(it->System.out.println(it);new BF().parser(it);System.out.println(""););

//       new BF().parser(cmds.get(4));

  


   /**
    * 当前指针
    */
   private int currentPointer = 0;

   /**
    * 模拟指针指向的数据带
    * 使用ArrayList数据结构来模拟,实现过程中,需要自己越界问题
    */
   private List<Integer> band = new ArrayList<>();


   /**
    * 存储输入的指令集,以便后续程序调研
    * */
   private char[] chars;

   /**
    * 指令指针
    * */
   private int currentCmdPointer = 0;

   /**
    * 将输入转换成指令集,并遍历执行指令集
    * */
   public void parser(String input)
       chars = input.toCharArray();
       for(currentCmdPointer = 0; currentCmdPointer < chars.length ; currentCmdPointer++)
           this.charcmd(chars[this.currentCmdPointer]);
      
  ;

   /**
    * 指令集
    * */
   private void charcmd(char c)
       switch (c)
           case '+': this.increase();break;
           case '-': this.reduce();break;
           case '>': this.forward();break;
           case '<': this.backward();break;
           case '.': this.print();break;
           case ',': this.input();break;
           case '[': this.whileEntity();break;
           case ']': this.whileEnd();break;
           default:break;
      
  
   /**
    * 循环结束
    * 如果当前指针指向的数据带值不为 0,则跳到与之匹配的 '['后一条指令
    * */
   private void whileEnd() 
       if(this.band.get(this.currentPointer)!=0)
           while(true)
               if(this.chars[--this.currentCmdPointer]=='[')
                   break;
              
          
      
  

   /**
    * 循环开始
    * 如果当前指针指向的数据带值为 0,则跳到与之匹配的 ']'后一条指令
    * */
   private void whileEntity() 
       if(this.band.get(this.currentPointer)==0)
           while(true)
               if(this.chars[this.currentCmdPointer++]==']')
                   break;
              
          
      
  

   /**
    * 获取键盘输入的字节流,写入当前指针指向的数据带
    * */
   private void input() 
       try 
           this.band.add(this.currentPointer, (int) System.in.read());
       catch (IOException e) 
           e.printStackTrace();
      
  

   /**
    * 将当前指针指向的数据带值的ASCII码打印
    * */
   private void print() 
       System.out.printf("%c",this.band.get(this.currentPointer));
  


   /**
    * 指针向又移一
    * */
   private void forward() 
       this.currentPointer++;
       //处理越界
       if(this.currentPointer >= this.band.size()-1)
           this.band.add(0);
      
  


   /**
    * 指针向左移一
    * */
   private void backward() 
       --this.currentPointer;
       //处理越界
       this.currentPointer = this.currentPointer < 0 ? 0 : this.currentPointer;
  

   /**
    * 当前指针指向的数据带值-1
    * */
   private void reduce() 
       Integer temp = this.band.get(this.currentPointer);
       temp--;
       this.band.set(this.currentPointer,temp);
  

   /**
    * 当前指针指向的数据带值+1
    * */
   private void increase() 
       //规避数据越界问题
       for(int i = this.band.size(); i<=this.currentPointer;i++)
           this.band.add(0);
      
       //获取当前指针值,+1,并写回
       Integer temp = this.band.get(this.currentPointer);
       temp++;
       this.band.set(this.currentPointer,temp);
  

执行结果演示

 

面参考链接里(https://fatiherikli.github.io/brainfuck-visualizer/#),有可视化的执行流程动画,更容易理解一些。

如果文章对您有一点点帮助,那么真诚的希望您能点赞,评论,转发,谢了!

参考链接:

https://www.zhihu.com/question/20115374

https://fatiherikli.github.io/brainfuck-visualizer/#

https://en.wikipedia.org/wiki/Turing_completeness

https://blog.csdn.net/qq_36936155/article/details/79347364

关注 【 麒思妙想】解锁更多硬核。

历史文章导读

如果文章对您有那么一点点帮助,我将倍感荣幸

欢迎  关注、在看、点赞、转发 

以上是关于手动实现一门图灵完备的编程语言——Brainfuck的主要内容,如果未能解决你的问题,请参考以下文章

寻找不是图灵完备的语言

C++ 预处理器元编程图灵完备吗?

bzip2图灵完备吗?

“非图灵完备”到底意味着什么

GPU着色器图灵完备吗

如果将markdown视作一门编程语言可以做哪些有趣的事情?