二进制加一:n ↦ n+1
父节点:examples
思路:从最右边的比特开始,把末尾连续的 1 翻转成 0(进位),再把遇到的第一个 0(或空白)翻转成 1。
七元组 (Q, Γ, b, Σ, δ, q₀, F)。
Q = {carry, done};q₀ = carry;F = {done}。Γ = {0, 1, b}(b= 空白);Σ = {0, 1}。δ——「操作手册」:
δ(carry, 1) = (carry, 0, L) a trailing 1 becomes 0, keep carrying left
δ(carry, 0) = (done, 1, R) first 0 absorbs the carry -> 1, stop
δ(carry, b) = (done, 1, R) ran off the left (e.g. 11 -> 100): blank -> 1
在输入 1011(= 11)上的执行轨迹,读写头位于最右边的比特(^ = 读写头;左侧为状态):
carry: 1 0 1 1 scanned Γ-symbol = 1
^ look up δ(carry,1)=(carry,0,L) [uses δ + Q]
carry: 1 0 1 0 wrote 0 (Γ); moved L; stayed carry (Q)
^
carry: 1 0 1 0 scanned 1 -> δ(carry,1)=(carry,0,L)
^
carry: 1 0 0 0 wrote 0; moved L
^
carry: 1 0 0 0 scanned 0 -> δ(carry,0)=(done,1,R)
^
done: 1 1 0 0 wrote 1; moved R; entered done
^
done ∈ F → 停机。输出 1100 = 12。✓
每一步是哪个部件在起作用: 读写头 + Γ 给出被扫描到的符号 → δ + Q 选定动作 → 写入的是 Γ,{L,R} 方向移动读写头,Q 随之更新 → 是否停机则由 F 检验。
用代码表达——地道的 Haskell
图灵机的 δ 字面上就是一个模式匹配函数,Q/Γ/{L,R} 是代数数据类型,纸带则是一个列表 zipper (left-reversed, current, right)。转移表一一对应地映射到各条匹配分支上:
data St = Carry | Done deriving Eq
data Mv = L | R
-- delta: each rule of the table is one pattern-match clause
delta :: St -> Char -> (St, Char, Mv)
delta Carry '1' = (Carry, '0', L) -- trailing 1 -> 0, carry left
delta Carry '0' = (Done, '1', R) -- first 0 -> 1, stop
delta Carry _ = (Done, '1', R) -- blank -> 1 (ran off the left)
-- tape zipper: ls is the left half reversed, c the scanned cell, rs the right
run :: St -> [Char] -> Char -> [Char] -> [Char]
run Done ls c rs = reverse ls ++ c : rs -- F-test: halt
run q ls c rs =
let (q', w, m) = delta q c -- look up delta
in case m of -- write w, then move
L -> case ls of (l:ls') -> run q' ls' l (w:rs)
[] -> run q' [] 'b' (w:rs)
R -> case rs of (r:rs') -> run q' (w:ls) r rs'
[] -> run q' (w:ls) 'b' []
-- run Carry (reverse "101") '1' "" ==> "1100" (11 + 1 = 12)
这份严丝合缝的对应正是要点所在:δ = 模式匹配,状态 = 一个 ADT,纸带 = 一个 zipper——图灵机就是一个微型的函数式程序。把 delta 换成另一张表,同一个 run 就会执行出一台不同的机器——这正是把 δ 当作数据来使用的通用机。