4

我在 Racket 中组装了以下基本秒表(现在刚刚学习,最终目标是番茄钟)。

#lang racket

(define start-time 0)
(define end-times '())

(define (start);; stores start-time
  (set! start-time (current-seconds)))

(define (lap);; stores "laps" in list
  (set! end-times (cons (current-seconds) end-times)))

(define (stop);; stores final time, displays lap-times in h, m, s and resets end-times
  (begin
    (set! end-times (cons (current-seconds) end-times))
    (display
     (reverse
      (map (lambda (an-end)
             (let ((the-date (seconds->date(- an-end start-time))))
               (list
                (sub1(date-hour the-date))
                ;; sub1 is needed because (date-hour(seconds->date 0) = 1
                (date-minute the-date)
                (date-second the-date)))) end-times)))
    (set! end-times '())
    ))

虽然这正是它应该做的,但我想知道如何避免可变状态。如果我遵循 HTDP,这是一种需要可变状态的情况,但是在浏览了 Wadler 的“函数式编程的 Monads ”之后,我仍然对没有set!.

我知道要使其正常工作,我应该为我的函数添加参数。例如,start将成为

(define (start [now (current-seconds)])
  now)

类似的方法可以与lapand一起使用stop

尽管如此,虽然我知道在添加额外的参数来恢复功能之后,我还应该传递参数而不是将值存储在变量中,但我不知道在这种情况下我可以如何利用它来避免set!

更新:由于以下所有三个答案都非常有价值(谢谢!),我没有将它们中的任何一个标记为唯一正确的答案。以下是我最初问题的最小解决方案。它是@Metaxal 的循环建议与@Greg Hendershott 的示例用法的组合。

#lang racket

(define (run)
  (displayln "Enter 'lap' or 'quit':")
  (let loop ([t0 (current-seconds)] [times '()])
    (match (read-line)
      ["quit" (reverse
      (map (lambda (x)
             (let ((the-date (seconds->date x)))
               (list
                (sub1(date-hour the-date))
                (date-minute the-date)
                (date-second the-date)))) times))]
      ["lap" (loop t0 (cons (- (current-seconds) t0) times))]
      [_ (loop t0 times)])))
4

3 回答 3

2

在你的程序下面可能发生的事情是你将有一个循环。那么这个循环可以是一个将整个当前状态作为输入的函数,当你想更新它的状态时,只需用新的状态再次调用循环(当然你也可以用相同的状态再次调用循环) .

简化示例:

(define (loop [t0 (current-seconds)] [times '()])
  ;; ... do things here, possibly depending on user input ...
  ;; then loop with a new state:
  (cond [<some-start-condition> (loop (current-seconds) '())]
        [<some-lap-condition>   (loop t0 (cons (- (current-seconds) t0) times))]
        [<some-stop-condition>  times])) ; stop, no loop, return value

不过,这肯定会改变您的设计方法。

在设计 GUI 程序时很难使用这种方法,因为事件循环通常会阻止您(或使其难以)将值从一个事件传递到下一个事件。然而,在 Racket 中,有(教学法,但仍然非常好)big-bang就是为此而生的。

于 2014-04-22T20:30:57.677 回答
1

对于像这样的简单示例,我可能会按照@Metaxal 的建议进行操作。

但是另一种方法是您可以将状态显式定义为struct

(struct state (start-time end-times))

然后将startlap和更改为 上的stop函数state

;; start : -> state
;; stores start-time
(define (start)
  (state (current-seconds) '()))

;; lap : state -> state
;; stores "laps" in list
(define (lap st)
  (match-define (state start-time end-times) st)
  (state start-time
         (cons (current-seconds) end-times)))

;; stop : state -> list
;; stores final time, displays lap-times in h, m, s
(define (stop st)
  (match-define (state start-time end-times*) st)
  (define end-times (cons (current-seconds) end-times*))
  (reverse
   (map (lambda (an-end)
          (let ((the-date (seconds->date(- an-end start-time))))
            (list
             (sub1(date-hour the-date))
             ;; sub1 is needed because (date-hour(seconds->date 0) = 1
             (date-minute the-date)
             (date-second the-date)))) end-times)))

正如@Metaxal 的回答一样,您的“主循环”需要处理状态并通过适当的函数“线程”它:

示例用法:

(define (run)
  (displayln "Enter 'lap' or 'quit':")
  (let loop ([st (start)])
    (match (read-line)
      ["quit" (stop st)]
      ["lap" (loop (lap st))]
      [_ (loop st)])))

而@Óscar López 的回答显示了 SICP 中解释的 OOP 风格。

关于 Racket(和 Scheme)的一个好处是,您可以选择您认为最适合手头问题和您的品味的任何方法——简单命令式、OOP 命令式、纯函数式。

于 2014-04-22T21:35:32.520 回答
1

在这种情况下,使用set!是合理且难以避免的,因为我们必须“记住”过程调用之间的状态。我们可以做的是改进状态的封装,通过隐藏过程中变化的变量并使用消息调度程序访问引用可变状态的过程。这与我们在面向对象编程中所做的非常相似,但只lambda需要 s 即可实现它!

(define (make-timer)

  ; the "attributes" of the object

  (let ([start-time  0]
        [end-times '()])

    ; the "methods" of the object

    (define (start)
      (set! start-time (current-seconds)))

    (define (lap)
      (set! end-times (append end-times (list (current-seconds)))))

    (define (stop)
      (lap)
      (display
       (map (lambda (an-end)
              (let ((the-date (seconds->date (- an-end start-time))))
                (list
                 (sub1 (date-hour the-date))
                 (date-minute the-date)
                 (date-second the-date))))
            end-times))
      (set! end-times '()))

    ; return a dispatch procedure

    (lambda (msg)
      (case msg
        ((start) (start)) ; call the start procedure defined above
        ((lap)   (lap))   ; call the lap procedure defined above
        ((stop)  (stop))  ; call the stop procedure defined above
        (else (error "unknown message:" msg))))))

我冒昧地修改了您的一些程序,以使它们更简单一些。下面是我们如何使用刚刚创建的计时器对象:

(define timer (make-timer))

(timer 'start)
(sleep 1)
(timer 'lap)
(sleep 1)
(timer 'lap)
(sleep 1)
(timer 'lap)
(sleep 1)
(timer 'stop)

=> ((18 0 1) (18 0 2) (18 0 3) (18 0 4))

这种技术称为“消息传递”,在精彩的SICP书中了解更多信息。

于 2014-04-22T20:24:10.033 回答