将队列表示为具有本地状态的过程

Representing a queue as a procedure with local state

提问人:Enlico 提问时间:6/6/2022 最后编辑:Will NessEnlico 更新时间:6/9/2022 访问量:109

问:

90页的§2.1.3节用一个非常清楚的例子解释了语言中的第一类函数使函数本身和数据从不同的角度来看是同一回事,或者引用本书:

自动将过程作为对象进行操作的能力提供了表示复合数据的能力。

在第 266 页,第 §3.3.2 节中的练习 3.22 提出了以下建议

我们可以将队列构建为具有本地状态的过程,而不是将队列表示为一对指针。本地状态将由指向普通列表的开头和结尾的指针组成。因此,该过程将具有以下形式make-queue

(define (make-queue)
  (let ((front-ptr ...)
        (rear-ptr ...))
    <definitions of internal procedures>
    (define (dispatch m) ...)
    dispatch))

使用此表示形式完成队列操作的定义并提供其实现。make-queue

我很容易想出以下内容(在这种情况下,我使用了名称而不是 and,因为我发现它更清楚):the-listlast-pairfront-ptrrear-ptr

(define (make-queue)
  (let ((the-list '())
        (last-pair '()))
    (define (dispatch m)
      (cond ((eq? m 'empty) (null? the-list))
            ((eq? m 'front) (if (null? the-list)
                                (error "can't take front of empty list")
                                (car the-list)))
            ((eq? m 'ins) (lambda (e)
                            (if (null? the-list)
                                (begin (set! the-list (list e))
                                       (set! last-pair the-list))
                                (begin (set-cdr! last-pair (list e))
                                       (set! last-pair (cdr last-pair))))
                            the-list))
            ((eq? m 'del) (begin
                           (if (null? the-list)
                               (error "can't delete from emtpy list")
                               (set! the-list (if (pair? the-list) (cdr the-list) '())))
                           the-list))
            ((eq? m 'disp) (display the-list)) ; added this for convenience
            (else "error")))
    dispatch))

(define (empty-queue? q) (q 'empty))
(define (front-queue  q) (q 'front))
(define (insert-queue! q e) ((q 'ins) e))
(define (delete-queue! q) (q 'del))
(define (display-queue q) (q 'disp))

这似乎效果很好......

...除了一个关键点!

在 §3.3.2 的开头,两个所需的变值器(作为队列接口的一部分)是这样定义的(我的重点):

(insert-queue! <queue> <item>)

在队列的后面插入项,并将修改后的队列作为其值返回

(delete-queue! <queue>)

删除队列前面的项,并将修改后的队列作为其值返回,如果队列在删除前为空,则发出错误信号。

我的解决方案不遵守定义的这些部分,因为两者都返回 ,这是裸列表,队列接口的实现细节。事实上,我的解决方案不支持这样的事情insert-queue!delete-queue!the-list

(define q (make-queue)) ; ok
(insert-queue! (insert-queue! q 3) 4) ; doesn't work
(delete-queue! (delete-queue! q))     ; doesn't work

而我认为它应该。

我想解决方案应该看到并返回该函数的变异版本。delete-queue!insert-queue!dispatch

我该怎么做?

函数编程 方案 SICP 突变 First-Class-Functions

评论

0赞 alinsoar 6/9/2022
在 MIT/SCHEME 中,这种数据结构的表示称为 .阅读 mit/scheme 中的捆绑包,它们在任何地方都用于实现 mit/scheme。bundle 是你上面写的内容的句法糖。Bundle

答:

1赞 Will Ness 6/9/2022 #1

没有必要。简单定义

(define (insert-queue! q e) 
  ((q 'ins) e)
  q) 

(define (delete-queue! q) 
  (q 'del)
  q)

不过,设计并不干净,因为这些队列不是持久的。新版本和旧版本共享相同的基础缓冲区 (list)。不再保留旧版本,只有当前版本。

因此,我们不会返回新的、修改后的队列;我们返回已变异的同一队列。从概念上讲,就是这样。在更低的级别上,我们返回相同的调度过程,该过程是同一闭包的一部分,该闭包对内部缓冲区具有相同的内部绑定,该缓冲区已发生突变。

顺便说一句,使用头部哨兵技巧,您是否从例如 而不是 ,通常会导致更简化、更清晰的代码。(list 1)'()

评论

0赞 Enlico 6/9/2022
我觉得自己很傻。
0赞 Enlico 6/9/2022
为什么再说了?
0赞 Will Ness 6/9/2022
只是我特有的英语。:)一次写两句话。
0赞 Will Ness 6/9/2022
顺便说一句,要看到“头哨兵”的实际应用,你可以在我的答案中搜索它。
0赞 Will Ness 6/9/2022
我的意思是,这本书谈到了返回修改后的队列,这听起来像是暗示在某个地方有旧的未修改版本,并且不再有,随着突变。所以那里有一些认知失调。也许这就是作者推动读者问自己的方式,它能否持久存在,以及如何持久。接口不会改变,只有实现(和语义)会改变。