初看这题颇为棘手,但是仔细分析后,还是可以通过代码来完美解决的。陈然更聪明的人在脑子中就能做分析了,但是向我这种笨蛋,还是得靠MIT-scheme解释器来帮忙解决。
首先我们假设书中208页的注释 164成立,即对存储单元的基本写入操作是互锁的,并且再加一条,读取与写入对于操作同一个存储单元也是互锁的,那么才可以去讨论这一小题。这样一来,Peter、Paul和Mary的对同一个balance的三种不同操作就可以抽象成六种分离的操作,即:
Peter_read
Peter_write
Paul_read
Paul_write
Mary_read
Mary_write
这样,他们如果交错进行,样本空间也就是在保证同一个人的读取操作在他的写入操作之前的前提下,六个基本操作的自由排列了。通过初等概率可以计算得到,这样得到的一个样本空间中一共有90个样本。那到底有哪90种情况呢?可以写一个简单的树状递归的过程进行统计,代码如下:
; 用于统计列表交错的可能情况
(define (count-demo . arrays)
; 用于储存情况列表
(let ((demo-result '()))
; 用树状递归统计可能的情况
(define (rec result arrays)
(let ((arys (filter
(lambda (ary) (not (null? ary)))
arrays)))
(if (null? arys)
(set! demo-result (cons result demo-result))
(for-each
(lambda (ary)
(rec
(append result (list (car ary)))
(map
(lambda (other)
(if (eq? other ary)
(cdr other)
other))
arys)))
arys))))
(rec '() arrays)
demo-result))
我试了一下,这个过程的通用性非常好,除了解决本题中的情况以外,还可以用来列出书中第210页中3.4.2节里的第一种情况:
(count-demo '(a b c) '(x y z))
同样的也可以用来得到第211页里的第二种情况:
(count-demo '(p1_read_1 p1_read_2 p1_write) '( p2_read p2_write))
因此这个过程是我用来解决这个问题的核心过程。
接下来,因为想到第188页中3.3.4——对于数字电路的并发模拟的时候,所用的环境表的方法,所以也可以在这里用同样的方法来模拟结果,只不过不用像那边那么复杂罢了。对于表的实现,书中已经讲解得够详细了,这里就不多做赘述。我在这里使用的是一张二维结构的表,首先是创建被模拟的balance单元的读与写:
; 修改存款
(define (set-balance! new)
(put 'bank 'balance new))
(define (get-balance)
(get 'bank 'balance))
然后是在这个的基础上抽象出需要被交错的过程,并写成一个抽象的类,中间步骤都存在环境表中:
; 过程步骤抽象(两步)
(define (make-process-2 name f)
; 模拟读取的过程
(put name 'read
(lambda ()
(put name 'now (get-balance))))
; 模拟写入的过程
(put name 'write
(lambda ()
(set-balance! (f (get name 'now)))))
; 返回特征表
(list (cons name 'read) (cons name 'write)))
; 过程步骤抽象(三步)
(define (make-process-3 name f)
; 模拟第一次读取的过程
(put name 'read-1
(lambda ()
(put name 'now-1 (get-balance))))
; 模拟第二次读取的过程
(put name 'read-2
(lambda ()
(put name 'now-2 (get-balance))))
; 模拟写入的过程
(put name 'write
(lambda ()
(set-balance! (f (get name 'now-1) (get name 'now-2)))))
; 返回特征表
(list (cons name 'read-1) (cons name 'read-2) (cons name 'write)))
这里分别给出了两种过程的模拟,前一种是可以分解成两部的过程的抽象,后一种是可以分解成三步的,当然,你也可定义一个更通用的类——可以抽象出N步的。但是在这里,两部的就够用了。
接下来是一个对可能性结果的封装类,使得每条结果更便于阅读与操作,所以里面充斥着各种的字符串转换与拼接。这样出结果之后,也就省去了题中需要画图的麻烦:
; 交错后可能性的抽象
(define (make-possibility demo)
; 得到过程
(define (get-proc k-v)
(get (car k-v) (cdr k-v)))
; 翻译单条操作信息
(define (make-info k-v)
(string-append
(symbol->string (car k-v))
"_"
(symbol->string (cdr k-v))))
; 组合两条操作信息
(define (add-info info1 info2)
(string-append
info1
"->"
info2))
(let ((proc-list (map get-proc demo))
(result 0))
(set-balance! 100)
;(set-balance! 10)
(for-each (lambda (proc) (proc)) proc-list)
(set! result (get-balance))
; 分派函数
(lambda (m)
(cond ((eq? m 'result) result)
((eq? m 'info)
(accumulate add-info "\b\b\t" (map make-info demo)))
(else (error "Unknown operation -- MAKE-RESULT" m))))))
因为可能的结果比较多,所以想到再写一个类专门用于对上述结果的分类与统计。为了贪图方便,也就没有再独立创建一个表格专门用于存放统计结果,而是直接将结果分类后存进了环境表中,其实这样做非常不好,非常不可取。还有请注意,这里必须使用accumulate而不能使用MIT-scheme解释器中自带的reduce,原因我在之前计算零钱的那篇文章中已经讲过了:
; 统计可能性的值
(define (make-statistician demos)
(let ((possibility-list (map make-possibility demos))
(result-list '()))
; 进行统计
(define (iter possibility-list)
(if (null? possibility-list)
'done
(let ((possibility (car possibility-list)))
(let ((result (possibility 'result))
(info (possibility 'info)))
(if (memq result result-list)
(put 'result result (cons info (get 'result result)))
(begin
(set! result-list (cons result result-list))
(put 'result result (list info))))
(iter (cdr possibility-list))))))
(iter possibility-list)
; 分派函数
(lambda (m)
(cond ((eq? m 'all) result-list)
((eq? m 'one)
(lambda (result)
(get 'result result)))
(else (error "Unknown operation -- MAKE-STATISTICIAN" m))))))
最后为了今后便于查看结果,对上述统计的结果,提供两个查询操作的接口:
; 操作接口
(define (one-poss statistician result)
(for-each
(lambda (info)
(newline)
(display info))
((statistician 'one) result)))
(define (all-poss statistician)
(statistician 'all))
接下来就是测试了,代码就可以完全按照面向对象的思维去写了,首先定义三个人的三种操作:
; 定义 Peter 的操作
(define Peter (make-process-2 'Peter (lambda (x) (+ x 10))))
; 定义 Paul 的操作
(define Paul (make-process-2 'Paul (lambda (x) (- x 20))))
; 定义 Mary 的操作
(define Mary (make-process-2 'Mary (lambda (x) (/ x 2))))
之后是创建一个统计者,用于对结果做统计与分析:
; 创建一个统计者
(define S (make-statistician (count-demo Peter Paul Mary)))
首先回答第一问,总共会出现哪些可能的值时,可以这样问解释器:
(all-poss S)
解释器就会用列表的形式将所有可能的值全部返回给你。如果你想了解其中某个值出现时可能对应的交错顺序,比如45,就可以这样问解释器:
(one-poss S 45)
则解释器就会将所有关于如何得到值45的交错情况在命令行中格式化打印给你。
再比如,书中211页中3.4.2里的第二种情况可以这样模拟:
; 定义过程1
(define p1 (make-process-3 'p1 *))
; 定义过程2
(define p2 (make-process-2 'p2 (lambda (x) (+ x 1))))
; 创建一个统计者
(define S (make-statistician (count-demo p1 p2)))
但是需要注意一点,得先将make-possibility类中的
(set-balance! 100)
给注释掉,改成
(set-balance! 10)
就可以了。