Functional programming 在需要多次存储(当前秒数)时避免可变状态

Functional programming 在需要多次存储(当前秒数)时避免可变状态,functional-programming,racket,monads,purely-functional,Functional Programming,Racket,Monads,Purely Functional,我已经在球拍中组装了以下基本的秒表(刚刚开始学习,最终目标是一个波莫多罗计时器) 虽然这正是它应该做的,但我想知道如何避免可变状态。如果我遵循HTDP,这是一种可以保证可变状态的情况,但是在浏览了Wadler的“”之后,我仍然好奇没有集我怎么办 我知道,要使其功能化,我应该在函数中添加参数。例如,start将成为 (define (start [now (current-seconds)]) now) 类似的方法可以用于lap和stop 不过,尽管我知道在添加额外参数以恢复功能后,我还应该

我已经在球拍中组装了以下基本的秒表(刚刚开始学习,最终目标是一个波莫多罗计时器)

虽然这正是它应该做的,但我想知道如何避免可变状态。如果我遵循HTDP,这是一种可以保证可变状态的情况,但是在浏览了Wadler的“”之后,我仍然好奇没有
集我怎么办

我知道,要使其功能化,我应该在函数中添加参数。例如,
start
将成为

(define (start [now (current-seconds)])
  now)
类似的方法可以用于
lap
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)])))

在这种情况下,使用
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))

这项技术被称为“消息传递”,请在这本精彩的书中进一步了解它。

在接下来的程序中可能会出现的情况是,您将有一个循环。 然后,这个循环可以是一个函数,它将整个当前状态作为输入,当您想要更新它的状态时,只需使用新状态再次调用该循环(当然,您也可以使用相同的确切状态再次调用该循环)

简化示例:

(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
(定义(循环[t0(当前秒)][次数'())
;;;…在这里做事情,可能取决于用户的输入。。。
;然后使用新状态循环:
(cond[(循环(当前秒))]
[(循环t0(cons(-(当前秒)t0)次))]
[时报]);停止,无循环,返回值
这当然会改变您的设计方法

在设计GUI程序时使用这种方法比较困难,因为事件循环通常会阻止(或使其难以)将值从一个事件传递到下一个事件。
然而,在球拍中,有(教学法,但仍然很好)就是为了这个而设计的。

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

但是,另一种方法是可以显式定义状态 作为结构体

(struct state (start-time end-times))
然后将
start
lap
stop
更改为
状态下的功能

;; 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)的一个好处是,您可以选择您认为最适合当前问题和您的口味的任何方法——简单命令式、面向对象命令式、纯功能性。

严格来说,这是对我问题的回答。(请参阅我的编辑,了解我如何将其与@Greg Hendershot的用法示例结合使用)我将回到SICP!事实证明,(阅读行)的使用是我没有想到的一件简单的事情,这使我无法把它做好。
(define (run)
  (displayln "Enter 'lap' or 'quit':")
  (let loop ([st (start)])
    (match (read-line)
      ["quit" (stop st)]
      ["lap" (loop (lap st))]
      [_ (loop st)])))