当我需要多次存储(当前 - 秒)时避免可变状态

时间:2014-04-22 19:59:15

标签: functional-programming racket monads purely-functional

我在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 for Functional Programming”之后,我仍然很好奇没有set!我该怎么做。

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

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

并且类似的方法可以与lapstop一起使用。

尽管我知道在添加其他参数以恢复功能之后,我还应该传递参数而不是将值存储在变量中,我不知道在这种情况下我是如何利用它来避免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)])))

3 个答案:

答案 0 :(得分: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就是为此而制作的。

答案 1 :(得分:1)

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

(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))

这种技术被称为&#34;消息传递&#34;,在精彩的SICP书中了解更多相关信息。

答案 2 :(得分:1)

对于这样一个简单的例子,我可能会做@Metaxal 建议。

然而,另一种方法是您可以明确定义状态 作为struct

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

然后将startlapstop更改为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)])))

@ÓscarLópez的答案显示了SICP中解释的OOP风格。

关于Racket(和Scheme)的一个好处是你可以选择你认为最适合手头问题的光谱方法,以及你的品味 - 简单的命令,OOP命令,纯粹的功能。