变动的状态
唯一不变的就是变化本身。 —斯宾塞·约翰逊
为了构造出更符合人们对真实世界认知的计算模型,我们引入了变动的数据。将这个世界看作是由许多独立的对象构成,每个对象各自保有自己的局部状态,并且随着时间变化。
对应到程序语言中,就相当于引入赋值预算符。从此以后,代换模型就不再适用了(因为我们不能期待同一个函数在不同的时间点返回相同的值),我们需要去考虑更加复杂的环境模型。
变动的队列(FIFO)
先进先出队列(FIFO),是一个典型的用来展示变动的数据结构的例子。
我们提供以下过程:
操作 | 效果 |
---|---|
make-queue | 创建空队列 |
empty-queue? (queue) | 判断是否空队列 |
front-queue (queue) | 返回队列的第一个元素(不修改队列) |
insert-queue! (queue) (item) | 将相应元素插入队列末端 |
delete-queue! (queue) | 删除队列前端的元素 |
下面是具体实现(common lisp)
;;queue
(defun front-ptr (queue)
(car queue))
(defun rear-ptr (queue)
(cdr queue))
(defun set-front-ptr! (queue item)
(setf (car queue) item))
(defun set-rear-ptr! (queue item)
(setf (cdr queue) item))
(defun empty-queue? (queue)
(null (front-ptr queue)))
(defun make-queue ()
(cons '() '()))
(defun front-queue (queue)
(if (empty-queue? queue)
(error "FRONTED called with an empty queue")
(car (front-ptr queue))))
(defun insert-queue! (queue item)
(let ((new-pair (cons item '())))
(cond ((empty-queue? queue)
(set-front-ptr! queue new-pair)
(set-rear-ptr! queue new-pair)
queue)
(t
(setf (cdr (rear-ptr queue)) new-pair)
(set-rear-ptr! queue new-pair)
queue))))
(defun delete-queue! (queue)
(cond ((empty-queue? queue)
(error "DELETE! called with an empty queue"))
(t
(set-front-ptr! queue (cdr (front-ptr queue)))
queue)))
变动的系统(数字电路模拟器)
我们采用的计算模型类似于真实世界中的对象:用来传递信号的导线,基本的部件:反相器,与门,非门,以及更复杂的功能块。
同样地,输入信号的改变会影响到输出信号,而不同的部件也会有着不同的输出延迟。
我们首先来构造最基本的,也是最麻烦的部件:导线。
按照我们刚才的分析,一个导线包含了两个局部状态变量,一个是当前的信号值,另一个是一组过程(用来影响该导线连接着的其他部件的状态),当信号值改变时,所有包含的过程都需要运行,以通知相连部件做出相应动作。
(defun make-wire ()
(let ((signal-value 0) (action-procedures '()))
#'(lambda (m)
(cond ((equal m 'get-signal) signal-value)
((equal m 'set-signal!)
#'(lambda (new-value)
(if (not (= signal-value new-value))
(progn (setf signal-value new-value)
(call-each action-procedures))
'done)))
((equal m 'add-action!)
#'(lambda (proc)
(setf action-procedures (cons proc action-procedures))
(funcall proc)))
(t (error "Unknown operation --WIRE"))))))
注意这里使用的前缀#’的λ-表达式,主要是为了提供闭包,让不同导线的信号值和过程互不影响。
比如下面这样写就不能提供闭包,而且会招致不易发觉的错误(不要问我怎么知道的):
(defun test ()
(let ((value 0))
(defun set-value (new-value)
(setf value new-value))
(defun dispatch (m)
(cond ((equal m 'get-value) value)
((equal m 'set-value) #'set-value)))
#'dispatch))
所有变量会共享同一个值:
围绕着导线,我们来完善一下相关过程:
(defun call-each (procedures)
(if (null procedures)
'done
(progn
(funcall (car procedures))
(call-each (cdr procedures)))))
(defun get-signal (wire)
(funcall wire 'get-signal))
(defun set-signal! (wire new-value)
(funcall (funcall wire 'set-signal!) new-value))
(defun add-action! (wire action-procedure)
(funcall (funcall wire 'add-action!) action-procedure))
其中,call-each会依次调用导线上的所有过程。从前面可以看到,每次导线上的信号值改变后,相关的过程都会进行依次调用,这很容易理解:在理想情况下,同一根导线上的各个部分都应该是相同的状态。
接下来,让我们来实现最基本的部件:
反相器:
;;反相器
(defun inverter (input output)
(defun invert-input ()
(let ((new-value (logical-not (get-signal input))))
(after-delay *inverter-delay*
(lambda ()
(set-signal! output new-value)))))
(add-action! input #'invert-input)
'ok)
;;逻辑反
(defun logical-not (s)
(cond ((= s 0) 1)
((= s 1) 0)
(t (error "Invalid signal"))))
反相器构造了一个反相的过程,这个过程会在相应的延迟时间后执行赋值的操作。然后将这个构造出来的过程加入在输入线的过程集合里。
与门和非门也类似:
;;与门
(defun and-gate (a1 a2 output)
(defun and-action-procedure ()
(let ((new-value
(logical-and (get-signal a1) (get-signal a2))))
(after-delay *and-gate-delay*
(lambda ()
(set-signal! output new-value)))))
(add-action! a1 #'and-action-procedure)
(add-action! a2 #'and-action-procedure)
'ok)
;;逻辑与
(defun logical-and (s1 s2)
(cond (