Intro

这是 sicp 第三章的一节 A Simulator for Digital Circuits,目的是设计一个数字模拟器,模拟了与或非门,半加器,全加器,行波进位加法器(ripple-carry adder)。虽然看起来跟程序设计无关,但其中涉及的事件驱动编程范式很值得学习。可以看到,上世纪八十年代,这种编程范式就已经被编入了教材之中。

事件驱动程序设计(英语:Event-driven programming)是一种计算机程序设计模型。这种模型的程序运行流程是由用户的动作(如鼠标的按键,键盘的按键动作)或者是由其他程序的消息来决定的。

示例

这里通过模拟一个半加器的执行来分析程序原理。通过自顶向下分析,看不懂的方法后面会解释,可以先从大体上了解这个系统。

Fig3.25c.std

半加器

先定义一个 agenda 和一些基础门的延迟。agenda 后面会说明,可以先简单地看作一种按照时间顺序来处理事件的数据结构。

(define the-agenda (make-agenda))
(define inverter-delay 2) ; 非门延迟
(define and-gate-delay 3) ; 与门延迟
(define or-gate-delay 5) ; 或门延迟

定义一些 wires,wires 会携带数字信号(digital signals),信号的值就是 0 或 1。

(define input-1 (make-wire))
(define input-2 (make-wire))
(define sum (make-wire))
(define carry (make-wire))

下面定义一个 probe procedure,用来探测 wire 上面信号的值,当 wire 上面信号发生改变时,它会打印出信号的值和其他信息。通过在 wire 上添加一个 action 来实现。

(define (probe name wire)
  (add-action! 
   wire
   (lambda ()
     (newline)
     (display name)
     (display " ")
     (display (current-time the-agenda))
     (display "  New-value = ")
     (display (get-signal wire)))))

调用 probe 在 sum 和 carry 这两条 wire 上,打印了它们各自的信息,信号值都为 0。

(probe 'sum sum)
sum 0  New-value = 0

(probe 'carry carry)
carry 0  New-value = 0

连接一个半加器

(half-adder input-1 input-2 sum carry)
ok

修改 input-1 上面的信号的值为 1

(set-signal! input-1 1)
done

调用 propagate 来开启事件循环,看到 sum 上面的信号值在 8 个单位时间后变成了 1。

(propagate)
sum 8  New-value = 1
done

再修改 input-2 上面的信号值为 1,carry 上面的信号在 11 个单位时间后变成了 1,sum 上面的信号在 16 个单位时间后变成了 0。

(set-signal! input-2 1)
done

(propagate)
carry 11  New-value = 1
sum 16  New-value = 0
done

分析

先脱离代码,通过半加器的图示来分析它的作用:

  • 一个半加器由一个或门,两个与门,一个非门组成

  • 接受输入信号 A, B,输出信号 S, C

  • 当且仅当 A 或 B 为 1,S 才为 1

  • 当 A 和 B 都为 1 时,C 才为 1

input-1 对应与 A, input-2 对应于 B,sum 对应于 S,carry 对应于 C。

所以上面的模拟结果是符合半加器特性的。

程序分析

给出半加器对应的 procedure,来分析一下程序是如何模拟半加器的执行。

(define (half-adder a b s c)
  (let ((d (make-wire)) (e (make-wire)))
    (or-gate a b d)
    (and-gate a b c)
    (inverter c e)
    (and-gate d e s)
    'ok))

;; logic gate
(define (and-gate a1 a2 output)
  (define (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)

(define (or-gate a1 a2 output)
  (define (or-action-procedure)
    (let ((new-value
            (logical-or (get-signal a1)
                        (get-signal a2))))
      (after-delay or-gate-delay
                   (lambda ()
                     (set-signal! output new-value)))))
  (add-action! a1 or-action-procedure)
  (add-action! a2 or-action-procedure)
  'ok)

(define (inverter input output)
  (define (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)

(define (logical-not s)
  (cond ((= s 0) 1)
        ((= s 1) 0)
        (else (error "Invalid signal" s))))

Wire

procedure 内部会有个变量 signal-value,在被更改且是不同值时,set-my-signal! 会调用内部保存的 action-procedures list 里的所有 procedure。

在调用 accept-action-procedure! 时,会添加 procedure 到 action-procedures list 里,并调用一次该procedure。

(define (make-wire)
  (let ((signal-value 0) 
        (action-procedures '()))
    (define (set-my-signal! new-value)
      (if (not (= signal-value new-value))
          (begin (set! signal-value new-value)
                 (call-each 
                  action-procedures))
          'done))
    (define (accept-action-procedure! proc)
      (set! action-procedures 
            (cons proc action-procedures))
      (proc))
    (define (dispatch m)
      (cond ((eq? m 'get-signal) 
             signal-value)
            ((eq? m 'set-signal!) 
             set-my-signal!)
            ((eq? m 'add-action!) 
             accept-action-procedure!)
            (else (error "Unknown operation: 
                          WIRE" m))))
    dispatch))

声明两个 wire,d 和 e。

调用 (or-gate a b d),对 a 添加一个 action,该 action 的作用是,当 a 或 b 的信号发生变化时,计算逻辑或的值,在一段延迟之后,设置 d 的值。这里也对 b 添加了相同的 action。也就是说只要输入端 a 或 b 信号发生了变化,就计算 a 和 b 的逻辑或值,传送到 d 输出端。

and-gateinverter 同理。所以,a, b 各自添加了 or-action-procedureand-action-procedure,c 添加了 invert-input,d 和 e 各自添加了 and-action-procedure

当 a 或 b 的信号发生改变,通过添加的 or-action-procedure,d 的值也被改变,通过添加的 and-action-procedure,c 的值也发生了改变。c 通过添加的 invert-input 改变了 e。d 或 e 通过添加的 and-action-procedure 改变了 s。

有一点还没讲,那就是 agenda 的概念。因为这些添加的 action,不会马上就被执行。

Agenda

agenda 是由 time-segment 组成的 list。每个 time-segment 是有 time 和 queue 组成的 pair。

agenda 的头节点时 current time,初始值时 0,调用 (first-agenda-item agenda) 后会更新为第一个 agenda item 的 time。

(add-to-agenda! time action agenda) 调用时,会根据 time 从小大到插入 action,如果找到了相同 time 的 segment,就添加到 segement 对应的 queue。如果 time 小于遍历中的一个 segment 的 time,就在这之前新建一个 segment,并插入。如果 time 大于所有的 segment 的 time,就新建一个 segment 到尾端。

前面的逻辑门里的 action procedure,会调用 (after-delay delay action) 来添加 action 到 agenda 中。time 为 agenda current-time 加上指定的 delay 时间。这样,添加到 agenda 的 action 都是按照 time 从小到大排序。

(define (after-delay delay action)
  (add-to-agenda! 
   (+ delay (current-time the-agenda))
   action
   the-agenda))

这也是为什么通过 add-action! 调用 (accept-action-procedure! proc) 需要执行一遍 (proc),因为不执行就不会把 action 添加到 agenda 中。对应于习题 3.31

 (define (or-gate a1 a2 output)
  (define (or-action-procedure)
    (let ((new-value
            (logical-or (get-signal a1)
                        (get-signal a2))))
      (after-delay or-gate-delay
                   (lambda ()
                     (set-signal! output new-value)))))
  (add-action! a1 or-action-procedure)
  (add-action! a2 or-action-procedure)
  'ok)

;; make-wire
(define (accept-action-procedure! proc)
     (set! action-procedures 
           (cons proc action-procedures))
     (proc))

agenda 程序

;; agenda operations
(define (make-agenda) (list 0))
(define (current-time agenda) (car agenda))
(define (set-current-time! agenda time)
  (set-car! agenda time))
(define (segments agenda) (cdr agenda))
(define (set-segments! agenda segments)
  (set-cdr! agenda segments))
(define (first-segment agenda) 
  (car (segments agenda)))
(define (rest-segments agenda) 
  (cdr (segments agenda)))

(define (empty-agenda? agenda)
  (null? (segments agenda)))

;; time segment
(define (make-time-segment time queue)
  (cons time queue))
(define (segment-time s) (car s))
(define (segment-queue s) (cdr s))


添加 action 和指定时间到 agenda

(define (add-to-agenda! time action agenda)
  (define (belongs-before? segments)
    (or (null? segments)
        (< time 
           (segment-time (car segments)))))
  (define (make-new-time-segment time action)
    (let ((q (make-queue)))
      (insert-queue! q action)
      (make-time-segment time q)))
  (define (add-to-segments! segments)
    (if (= (segment-time (car segments)) time)
        (insert-queue! 
         (segment-queue (car segments))
         action)
        (let ((rest (cdr segments)))
          (if (belongs-before? rest)
              (set-cdr!
               segments
               (cons (make-new-time-segment 
                      time 
                      action)
                     (cdr segments)))
              (add-to-segments! rest)))))
  (let ((segments (segments agenda)))
    (if (belongs-before? segments)
        (set-segments!
         agenda
         (cons (make-new-time-segment 
                time 
                action)
               segments))
        (add-to-segments! segments))))

访问 agenda 的第一个 item

(define (first-agenda-item agenda)
  (if (empty-agenda? agenda)
      (error "Agenda is empty: 
              FIRST-AGENDA-ITEM")
      (let ((first-seg 
             (first-segment agenda)))
        (set-current-time! 
         agenda 
         (segment-time first-seg))
        (front-queue 
         (segment-queue first-seg)))))

移除第一个 agenda item,会先从 queue 里移除 action,如果 queue 为空,移除这个 segment。

(define (remove-first-agenda-item! agenda)
  (let ((q (segment-queue 
            (first-segment agenda))))
    (delete-queue! q)
    (if (empty-queue? q)
        (set-segments! 
         agenda 
         (rest-segments agenda)))))

事件循环

事件循环就是一段不断调用自己的代码,直到 agenda 为空。如果不为空,取出 agenda 的第一个 item,并执行它,接着再调用自己。目的是按照时间顺序调用完 agenda 里的所有 item。

(define (propagate)
  (if (empty-agenda? the-agenda)
      'done
      (let ((first-item 
             (first-agenda-item the-agenda)))
        (first-item)
        (remove-first-agenda-item! the-agenda)
        (propagate))))

总结

通过上面精巧的例子,可以看出,事件驱动程序设计是非常”响应”的,通过执行一个事件,到触发另一个事件,这些都是自动会发生。而具体编码是通过类似观察者模式,改变一个对象的值,触发一系列 lambda 的调用,这些 lambda 的调用又改变了其包含变量的值,进而又触发了一系列 lambda 的调用。看起来很像我之前分析的 FBLPromise 例子。看起来这也是一种很通用的编程方法。