刚刚拜读了一本书, 《图灵的秘密》. 该书介绍了图灵的论文《论可计算数及其在判定上的应用》, 其指出: 一个拥有铅笔, 纸和一串明确指令的人类计算者, 可以被看做是一种图灵机
. 那么图灵机
是什么呢? 是图灵为了描述可计算数
而引出的一个虚构的可以做一些简单操作的计算机器. 尽管这个机器很简单, 但图灵断言它再功能上等价与一个进行数学运算的人.
先提个小醒, 文章有些长, 而且还比较枯燥.
当然了, 那些数学证明并不是我关心的, 我关心的是这个图灵机
. 图灵为了说明他的数学理论, 描述了一台机器, 而这台机器, 看过之后发现其实已经算是现代计算机的雏形了, 虽然他的出发点和发明计算机并不沾边吧. 先简单介绍一下这台机器.
这是一个配有一条纸带的机器, 纸带由一个个方格隔开, 图灵只使用其中相见的格子打印数据, 暂且称之为数字格, 数字格之间的用来辅助计算. 大概就这么一个纸带:
而这台机器对操作的定义由一张状态表来决定:
状态 | 符号 | 操作 | 状态切换 |
---|---|---|---|
其中每个状态(原文为: 格局)对应的符号不同, 会执行不同 的操作并切换的对应的下一个状态. 可以将状态类比成不同的函数, 再不同的选择分支下执行不同的逻辑并调用不同的函数. 下面给出一些符合和操作的定义:
符号
- 0/1 : 指定字符
- Any: 非空的任意符号
- None: 空
- 留空: Any 和 None
- else: 状态的其他符号都没有匹配
操作
- R: 向右移动一格
- L: 想左移动一格
- P: 打印字符(P0, 打印0)
- E: 擦除当前格子内容
OK, 对图灵这个简单的机器介绍完毕, 是不是特别简单. 一起跟着图灵来看看, 他在这台机器上都能够做些什么操作吧.
打印序列010101...
先给出一格简单的例子, 来看看这台机器是如何运行的. 打印序列01010101...
, 在前面加一个小数点, 这就是二进制的1/3了, 而将0和1的位置互换之后, 就是2/3了. 来看看图灵是如何实现这一功能的.
状态 | 符号 | 操作 | 状态切换 |
---|---|---|---|
b | None | P0,R | c |
c | None | R | e |
e | None | P1,R | f |
f | None | R | b |
对了, 图灵的机器运行都是从状态b
开始的. 让我们一起跟着状态表走起来. (图灵只使用相间的各自来打印数字, 数字格中间的用来辅助计算, 数字格不可更改)
1.展示纸带的初始状态
其中红色方块标记机器当前的扫描方格.
2.当前状态: b
, 打印0并向右移动一格, 切换状态: c
3.当前状态c
, 向右移动一格, 切换状态: e
4.当前状态e
, 打印0并向右移动一格, 切换状态: f
5.当前状态 f
, 向右移动一格, 切换回状态: b
此时, 切换回了初始的状态, 然后周而复始的打印下去. 当然, 上述状态是可以进行简化的, 通过当前方格的不同符号, 可以进行不同的操作, 简化后的状态表:
状态 | 符号 | 操作 | 状态切换 |
---|---|---|---|
b | None | P0 | b |
b | 0 | R,R,P1 | b |
b | 1 | R,R,P0 | b |
简单试一下, 简化后的状态实现的功能完全一样.
当然, 这个例子实在太简单了, 不过为了理解图灵这台机器, 还是有必要介绍一下的.
打印序列 001011011101111...
在这个序列中, 1的数量会依次加一, 也就是说要让这台机器再这个一维的纸带上记住前面打印了多少个1. 那么图灵是如何做到这一点的呢?
终于, 前面没有用到的非数字格出场了, 它用来辅助打印.
状态 | 符号 | 操作 | 状态切换 |
---|---|---|---|
b | Pa,R,Pa,R,P0,R,R,P0,L,L | e | |
e | 1 | R,Px,L,L,L | e |
e | 0 | q | |
q | Any | R,R | q |
q | None | P1, L | p |
p | x | E,R | q |
p | a | R | f |
p | None | L,L | p |
f | Any | R,R | f |
f | None | P0,L,L | e |
老规矩, 直接来走一遍状态表, 边走边分析.
1. 当前状态 b
, 操作Pa,R,Pa,R,P0,R,R,P0,L,L
, 切换状态e
可以发现, 状态b
在初次执行之后, 就再也没有出现过了, 所以可以将其形容为初始化的操作.
2.当前状态e
, 符号0
,直接切换状态: q
3.当前状态q
, 符号0
, 操作: R,R
, 切换状态q
4.当前状态q
, 符号0
, 操作: R,R
, 切换状态: q
5.当前状态 q
, 符号None
, 操作: P1, L
, 切换状态p
可以看到, q
状态的操作就是向当前序列的尾部添加一个1.
6.当前状态 p
, 符号None
, 操作: L,L
, 切换状态p
这不的操作就是向左移动到第一个不为空的非数字格. 省略一步, 结果:
7.当前状态p
, 符号a
, 操作: R
, 切换状态: f
从这里可以看出来, p
状态其实是充当各个状态之间的调度员角色的.
8.当前状态f
, 符号0
, 操作: R,R
, 切换到状态f
这里, 可以观察到, 状态f
的作用是向数字的结尾打印0, 然后左移两格, 切换到e
状态. 跳过中间步骤:
**9.当前状态e
, 符号1
, 操作: R,Px,L,L,L
, 切换状态e
. **
简单分析, e
状态的操作是在连续1
的每一个右边打印x
, 之道遇到0
, 切换到q
状态
10.当前状态q
, 符号0
, 则向尾部添加一个1, 切换状态p
**11.当前状态p
, 符号None
**
通过前面分析, p
状态向左找到第一个不为空的非数字格, 在这里则是x
. 然后会擦除x
并切换到q
状态, 既向右打印1
. q
状态执行完毕后的纸带状态如下:
此时会再次切换回p
状态, 向左找到a
, 然后切换到f
状态, 向右打印一个0
.
完美, 此时其实已经发现了, 图灵的方法是在连续1的后面添加x
标记, 每个x
标记都对应一格末尾的1
. 以此来获得上一次打印1
的数量.
至此, 这台简单的机器已经能够记忆一些内容了.
数字递增
至此, 图灵这台机器虽然已经能够打印一些复杂的内容了, 不过都是一些简单的重复工作, 还没有能够称之为计算的东西. 为了引出后面的计算, 先来实现一个简单的数字递增.
这是一个不符合图灵约定的机器, 加在这里只是为了引出计算. 而且他的最高位数字向左打印. 来看一下这个状态表:
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
b | None | P0 | i |
i | 0 | P1 | r |
i | 1 | P0,L | i |
i | None | P1 | r |
r | None | L | i |
r | Any | R | r |
这个简单的例子就不再展开步骤了, 可以动手试一下, 它确实实现了数字的递增. 从这里能够看出使用二进制的好处, 如果使用十进制相同的功能, 则i
状态需要列出0-9
始终状态十种状态, 而二进制则只需要两种.
计算
好了, 论文中首个可以称为计算的例子来了.
是一个无限不循环小数.
先来介绍一下在计算
时涉及的数学知识. 首先,
一定是介于1-2之间的一个小数. 二进制的
前十位是: 1.011. 如何确定下一位是0还是1呢? 方法听上去很简单, 先假设下一位是1, 然后让这个 n 位数与自身相乘, 若结果是2n-1
位, 则说明结果小于2, 下一位是1. 若结果是2n
位, 则大于2, 下一位是0. 这个很好证明, 可以自己动手算一下.
而一个 n 位数与自身相乘, 需要将其中的每一位与每一位相乘, 共需要计算n*n
次. 产生n个局部乘积, 然后将局部乘积进行相加得到结果.
而图灵在计算时, 使用了稍有不同的方法进行乘法计算, 在运算中维护一个过程和, 每一位的相乘结果加到这个过程和中. 当然, 每一个位与位的乘积, 并不是加到过程和的最低位, 而是加到中间的某个位置上.
二进制的乘法很简单, 1*1=1, 其他情况都是0. 而乘积加到过程和的哪一位, 如果右起第2位(从0开始)乘以第3位, 则加到结果和的第2 3=5
位上. 先说明, 在下方的过程和计算中, 过程和的最低位在左侧, 与数字格的顺序相反, 应该是为了简化计算过程吧.
来一起看看图灵是如何实现这个功能的呢? 这次就不直接上状态表了, 这个表有点长, 就拆成多个放到分析中了. 如果有感兴趣的, 将下方所有状态合起来就是完整的状态表了.
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
begin | None | Pa,R,P1 | new |
为了方便介绍, 我们就不从头跑一遍了, 太费篇章. 我们就从中间位置开始来看一下他的计算过程, 具体过程可以自己跟着状态走一遍. 假设现在已经计算出了三位1.01
.
其中?
标识当前需要计算的下一位, 并没有实际出现在纸带上. 每次计算新的一位, 都会调用new
状态将扫描格重置到最左边的数字上:
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
new | a | R | mark_digits |
new | else | L | new |
假设此时, 纸带的状态:
现在对各个数字位进行标记.
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
mark_digits | 0 | R,Px,R | mark_digits |
mark_digits | 1 | R,Px,R | mark_digits |
mark_digits | None | R,Pz,R,R,Pr | find_x |
很简单, 在所有已知位后面都标记x
. 未知位标记z
, 然后在过程和的最低位打印r
.
当前状态: find_x
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
find_x | x | E | first_r |
find_x | a | find_digits | |
find_x | else | L,L | find_x |
first_r | r | R,R | last_r |
first_r | else | R,R | first_r |
last_r | r | R,R | last_r |
last_r | None | Pr,R,R,Pr | find_x |
r
是过程和的最低位, 可以将其看做0. 接下来的步骤, 会将每一个x
都对应的打印两个r
. 也就是说, 现在数字一共4位(包括?, 其为1). 然后打印了7个 r (2n-1). 根据之前的推测, 若结果需要新的一位, 则值为0, 否则为1.
当前状态: find_digits
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
find_digits | a | R,R | find_first_digit |
find_digits | else | L,L | find_digits |
find_first_digit | x | L | found_first_digit |
find_first_digit | y | L | found_first_digit |
find_first_digit | z | L | found_second_digit |
find_first_digit | None | R,R | find_first_digit |
如果未知位是1
, 那么过程和7位就够用了, 否则未知位就是0. 现在, 已经有了都是0的7位过程和, 可以开始做乘法运算了.
现在, 开始准备首次的位与位相乘了. 为了记录当前是那两位在做乘法运算, 使用x
, y
, z
进行标记. 其中x
标记与y
标记做乘法运算, 若自身相乘, 则标记为z
. 先通过find_digits
回到第一个非数字格. 然后通过find_first_digit
跳到第一个做乘法运算的数字格. 并根据标记的不同调用不同的方法.
当前状态: found_second_digit
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
find_second_digit | x | L | found_second_digit |
find_second_digit | y | L | found_second_digit |
find_second_digit | None | R,R | find_second_digit |
found_first_digit | 0 | R | add_zero |
found_first_digit | 1 | R,R,R | find_second_digit |
found_second_digit | 0 | R | add_zero |
found_second_digit | 1 | R | add_one |
found_second_digit | None | R | add_one |
这里可以看到, 若找到的数字是0, 则直接加0, 因为相乘后的结果必是0. 若相乘之后的结果是1, 则向过程和加1.
若找到的第一个数字是1, 则转换去寻找第二个数字.
当前状态: add_one
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
add_zero | r | Ps | add_finished |
add_zero | u | Pv | add_finished |
add_zero | else | R,R | add_zero |
add_one | r | Pv | add_finished |
add_one | u | Ps, R,R | carry |
add_one | else | R,R | add_one |
虽然给过程和中加0并不会对其值造成改变, 但是不管想其中加入了什么, 机器都需要对其进行一些维护. 之前说过, 过程和的r
表示0, 其实s
, t
也表示0, 对应的, u
, v
, w
则表示1. 为什么需要多个字母来表示同一个数字呢? 是为了下一次加法运算时, 用来标识当前数字已经参与过运算, 应该将结果加到下一位上.
add_zero
会将它找到的第一个r
标记为s
, 或者找到的第一个u
标记为v
. 然后结束加法操作.
而向过程和中加一, 则需要更多的操作, 毕竟数字已经变了嘛, 而且还需要处理进位. 将找到的第一个r
变成v
(0变成1), 或者找到的第一个u
变成s
(1变成0)并同时处理进位.
当前状态: add_finished
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
add_finished | a | R,R | erase_old_x |
add_finished | else | L, L | add_finished |
erase_old_x | x | E,L,L | print_new_x |
erase_old_x | z | Py,L,L | print_new_x |
erase_old_x | else | R,R | erase_old_x |
print_new_x | a | R,R | erase_old_y |
print_new_x | y | Pz | find_digits |
print_new_x | None | Px | find_digits |
erase_old_y | y | E,L,L | print_new_y |
erase_old_y | else | R,R | erase_old_y |
print_new_y | a | R | new_digit_is_one |
print_new_y | else | Py,R | reset_new_x |
此时, 加法结束了, 需要移动x
, y
, z
标记, 来标识下一对需要相乘的数字. 简单设想一下, 若只有一个z
则将其标记为y
并将左侧标记为x
即可. 若一个x
一个y
, 则将x
左移一位, 但是当x
到达最高位时, 需要将x
重置到最低位, 同时左移y
. 当y
到达最左侧的时候, 计算结束.
当前状态: find_digits
现在, 计算又回到了最初的状态, 可以开始进行新一轮的计算了. 这次相乘的结果1*1=1
, 再次向过程和中加一. 结果:
继续执行后, 需要下面几个状态(下面为每次回到find_digits
状态的纸带情况)
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
reset_new_x | None | R,Px | flag_result_digits |
reset_new_x | else | R,R | reset_new_x |
flag_result_digits | s | Pt,R,R | unflag_result_digits |
flag_result_digits | v | Pw,R,R | unflag_result_digits |
flag_result_digits | else | R,R | flag_result_digits |
unflag_result_digits | s | Pr,R,R | unflag_result_digits |
unflag_result_digits | v | Pu,R,R | unflag_result_digits |
unflag_result_digits | else | find_digits |
可以看到, 当x
重置的时候, 下一次位与位相乘的结果需要加到过程和的第二位上, 因此, 需要对过程和的内容做少许修改: 第一个s
或v
变成t
或w
, 剩下的s
或v
变成r
或u
. 为了下一次计算的时候, 能够将结果加到对应的位置上, 就是下一次相乘结果的相加位要向后一格, 在做加一操作的时候, 只识别r
, u
, 所以之后的标识符还需要重置.
操作已经简单的走了, 是时候将所有状态放出来了.
状态 | 符号 | 操作 | 切换状态 |
---|---|---|---|
carry | r | Pu | add_finished |
carry | u | Pr,R,R | carry |
carry | None | Pu | new_digit_is_zero |
new_digit_is_zero | a | R | print_zero_digit |
new_digit_is_zero | else | L | new_digit_is_zero |
print_zero_digit | 0 | R,E,R | print_zero_digit |
print_zero_digit | 1 | R,E,R | print_zero_digit |
print_zero_digit | None | P0,R,R,R | cleanup |
new_digit_is_one | a | R | print_one_digit |
new_digit_is_one | else | L | new_digit_is_one |
print_one_digit | 0 | R,E,R | print_one_digit |
print_one_digit | 1 | R,E,R | print_one_digit |
print_one_digit | None | P1, R,R,R | cleanup |
cleanup | None | new | |
cleanup | else | E,R,R | cleanup |
其中carry
的操作就是进位操作, 当遇到符号1
时, 将其改为0
继续进位, 当遇到符号0
的时候, 则改为1
, 结束. 若遇到空内容, 说明计算产生第8位了, 则未知位必为0, 直接结束计算.
从上面看到, 还有一种结束情况就是y
走到头了, 这时没有产生进位, 说明未知位为1.
剩余的几个状态就一带而过了, 未知位是1, 未知位是0, 以及cleanup
清理所有过程和的内容.
整个乘法会以两种情况结束:
- 当进位产生新的位数时, 结束. 未知位是0
- 当
y
标记下一步走到头了, 结束. 未知位是1
至此, 已经完成了计算
的操作. 这个状态可以周而复始的一直计算下去. 不再继续延时了, 感兴趣的可以自己按照上面的状态表算一遍. 看了上面命名的状态, 有没有觉得和函数很像呀.
其实其原理并不复杂, 就是进行0和1的简单尝试, 然后根据结果的大小来决定后一位是什么内容. 但我还是被图灵能够在一维的纸带上实现的操作折服了.
方法集
看了上面的内容, 有没有觉得少了点什么? 那一个个的不就是函数嘛.而图灵下一步要做的, 就是要组建一格常用表集, 作为基础来搭建一些更为复杂的表. 更形象的说法就是封装函数. 同时, 函数的封装也方便他后面构建更大的程序. 关于函数的概念就不在赘述了, 天天用的也不少了. 就给出图灵的表达形式, 大家自然能够看懂.
回看一下上面的new_digit_is_zero
和new_digit_is_one
两个函数, 函数化之后的标识:
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
print_digit(A) | 0 | R,E,R | print_digit(A) |
print_digit(A) | 1 | R,E,R | print_digit(A) |
print_digit(A) | None | P(A),R,R,R | cleanup |
很好理解哈, 就不解释了. 同时, 其参数也可以传递状态. 将这个基础表称之为骨架表. 可以看的出来, 所有的骨架表都可以转换成不带参数的形式. 到这里, 其实和现在的函数式编程思想已经很接近了有木有.
举个栗子:
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
f(S, B, a) | α | L | f1(S, B, a) |
else | L | f(S, B, a) | |
f1(S, B, a) | a | S | |
None | R | f2(S, B, a) | |
else | R | f1(S, B, a) | |
f2(S, B, a) | a | S | |
None | R | B | |
else | R | f1(S, B, a) |
其中 α 用来标识开始.
来看一下这个骨架表是做什么用的? 简单分析一下:
- f 函数: 向左找到标识符α, 然后转到 f1函数
- 将扫描格进行定位
- f1函数: 向右找, 若找到 a, 执行 S 函数, 空格向右转到 f2函数, 否则继续向右寻找
- 找到向右的第一个 a, 执行 S 函数
- f2函数: 向右找, 若找到 a, 执行 S 俺叔, 空格向右执行 B 函数, 否则向右转到 f1函数
- 找到向右的第一个 a, 执行 S 函数
- 若找到连续两个空格, 执行 B 函数(与 f1函数配合, 识别连续的两个空格)
可以看出, f 就是 find, 他会寻找 a(也是参数), 若找到, 执行 S, 没找到则执行 B.
再来看一个栗子:
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
e1(S) | E | S | |
e(S, B, a) | f(e1(S), B, a) |
看这个骨架表. 函数 e1 的作用是将符号擦除, 然后转到 S 状态.
那么相对的, e 函数的作用是, 向右找到第一个 a, 若找到了则擦除并转到 S, 若没有找到, 转到 B. 同时, 图灵还允许一个函数同时接收不同个数的参数(想到了什么? 函数重载)
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
e(B, a) | e(e(B, a), B, a) |
这个两个参数的e
函数是不是有点皮了. 来分析, 三参数的e
, 作用是找到 a 符号并擦除, 然后转到 S. 再看两参数的e
函数, S 是什么? 还是他自己, 继续找一个 a 符号并擦除, 直到擦除调所有的 a.
也就是说, 这个函数实现的功能是, 擦除所有的 a, 然后转到 B. 从这可以看出, 图灵的思想对现在的编程提供了一定思路, 已经有函数的嵌套调用了, 膜拜了.
再来看一些定义的基础库, 来帮助理解图灵的这个概念.
找到出现的最后一格 a
函数 f 从左向右查找, 函数 g 从右向左找.
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
g(S) | Any | R | g(S) |
None | R | g1(S) | |
g1(s) | Any | R | g(S) |
None | S | ||
g1(S, a) | a | s | |
else | L | g1(S, a) | |
g(S, a) | g(g1(S, a)) |
其中单参数的函数 g 和单参数的函数 g1配合, 将扫描格移到最右侧.
在结尾打印
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
pe(S, b) | f(pe1(S, b), S, α) | ||
pe1(S, b) | Any | R,R | pe(S, b) |
None | Pb | S |
其中, 这个函数有一个假设, 就是最左侧有两个连续α, f 函数先将扫描格移动到最左侧α, 然后右移一格开始寻找, 这时当前格就是α.
在结尾打印两个字符
状态 | 下一个状态 |
---|---|
pe2(S, a, b) | pe(pe(S, b), a) |
直接先在结尾打印 a, 然后在在结尾打印 b
增强 find 函数
f 函数在找到所需字符后, 将扫描格向左或向右移动.
状态 | 操作 | 下一个状态 |
---|---|---|
l(S) | L | S |
r(S) | R | S |
fl(S, B, a) | f(l(S), B, a) | |
fr(S, B, a) | f(r(S), B, a) |
复制字符
找到用 a 标记的字符, 复制到结尾. 然后调用 S 函数.
状态 | 符号 | 下一个状态 |
---|---|---|
c(S, B, a) | fl(c1(S), B, a) | |
c1(S) | β | pe(S, β) |
这里有一个特殊的地方, c1函数中, 符号β表示扫描到的字符. 当然也可以将不同的字符进行排列(可能只有0或1).
复制并擦除
状态 | 下一个状态 |
---|---|
ce(S, B, a) | c(e(S, B, a), B, a) |
ce(B, a) | ce(ce(B, a), B, a) |
其中三参数的ce
, 会找到 a 标记的符号, 并复制到结尾. 然后调用e
擦除 a 标记. 擦除后执行第一个参数的状态. 而在两参数的ce
中, 传递过期的第一个参数是它自己. 就是说, 两参数的ce
会将所有 a 标记的符号复制, 同时擦除 a 标记, 最终转到 B.
(在之前打印'001011011101111...'的例子中, 就可以使用这个函数对1进行复制)
看到这里已经发现了, 图灵机令人咋舌的效率问题. 为了执行以此复制并擦除, 函数 c 会先调用 f 遍历一遍(f 函数会先回到开头的位置)进行复制操作, 然后再调用 f 进行遍历并将其擦除. 而复制擦除下一个字符, 又要重复这些操作. 如果在第一次找到的时候, 就顺手擦除, 然后再进行复制, 岂不更好.
不过, 想必图灵在当时并没有考虑效率的问题, 或者说他并不关心效率的好坏, 毕竟连这台机器都是想象出来的. 现在, 图灵已经可以构建一格函数库了, 类似与现在的系统库, 想必能够构造更为强大的机器了.
数字化
接下来, 图灵对他的表格进行了重新定义. 他先是证明了所有状态都可以拆解成如下所示的三个状态:
状态 | 符号 | 操作 | 符号 | 标识 |
---|---|---|---|---|
qi | Sj | PSk, L | qm | N1 |
qi | Sj | PSk, R | qm | N2 |
qi | Sj | PSk | qm | N3 |
其中 Sk 用来表示符号. 规定:
- S0 : 表示空格
- S1 : 表示0
- S2 : 表示1
- 其他: 一些自定义符号
其中的操作是:
- N1 : 打印并左移
- N2 : 打印并右移
- N3 : 打印
疑问, 改成这样真的能够表示之前的所有操作么? 举例一下 :
- 擦除操作: 既打印空格. PS0
- 左移操作: 既打印格子原本的符号.
而之前的一些较长的操作, 通过拆解也可以拆成这样的基本形式. 然后, 图灵定义了这么一格五元组:
qiSjSkLqm 用来直接表示上方的 N1 . 无用说也能和上面的表格对上吧. 有没有想到图灵要做什么? 这里每一个五元组, 都对应一个操作, 如果将多个五元组连起来, 并用分号隔开, 是不是就能完整的描述一个程序了.
至此, 他的状态表已经经过了一次转换, 变成了下标的形式. 接下来, 图灵要把下标去掉. 替换:
- qi -> D 后面 i 个 A
- Sj -> D 后面 j 个 C
如此一来, 他的机器就只剩下以下几个字符: D
, A
, C
, L
, R
, N
, ;
. 其中N
表示不移动. 图灵将这样的描述称为标准描述.
再然后, 图灵将仅存的这7个字符, 用1-7的数字来表示. 既: 1(A), 2(C), 3(D), 4(L), 5(R), 6(N), 7(;). 那么, 将得到一个完全由数字组成的完成程序. 而这些数字连起来, 就是一个比较大的整数, 也就是说, 图灵用一个整数来完全表示了他的机器. 这个数字被图灵称为描述数.
也就是说, 一个描述数可以唯一确定一个程序, 而一个程序可以对应多个描述数(因为状态的顺序是可以随意更换的). 同时, 这也说明通过枚举所有的整数, 可以得到所有可能的计算序列.
其实与现在的程序有着异曲同工之妙, 现在的程序也不过是一串二进制的数字.
可编程通用机
接下来, 图灵描述了一个可编程的通用机器, 将程序1的描述数放到纸带的开头, 机器 A 通过读取并完全复刻所有格局来实现程序1的功能. 这台机器可以通过读取不同的输入纸带, 来实现不同程序的功能.
同时, 图灵证明了这样一个机器的可行性. 现在, 假设需要执行的程序是之前的交替打印0和1的程序, 其状态表如下:
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
b | None | P0 | b |
0 | R,R,P1 | b | |
1 | R,R,P0 | b |
转换成通用格局之后:
状态 | 符号 | 操作 | 下一个状态 |
---|---|---|---|
q1 | S0 | PS1,R | q2 |
q2 | S0 | PS2,R | q1 |
很简单, 打印0然后打印1, 交替进行. 将通用格局转换成可描述符号:
- q1S0S1Rq2: DADDCRDAA
- q2S0S2Rq1: DAADDCCRDA
输入纸带如下所示(一行显示不下, 但他还是一维纸带哦):
每一条指令由五个连续部分组成:
- D 接 n 个 A: 表示状态, 最少由一个 A
- D 接 n 个 C: 表示识别符号
- D 接 n 个 C: 表示在扫描格打印的符号
- L/R/N: 表示扫描头的移动方向
- D 接 n 个 A: 表示下一个切换的状态.
接下来的证明过程, 就有些超出我的理解了, 感兴趣的朋友可以自行钻研一下, 我是看了好久, 也没搞懂.
至此, 图灵的这台机器, 其实已经有了现代计算机的雏形了. 而我, 也被这几十年前伟大的思想折服了. 牛批...
同时, 图灵的论文后面还使用这台通用机器进行了一些证明, 不过, 那并不是我所关心的内容.