Избегайте изменяемого состояния, когда мне нужно хранить (текущие секунды) несколько раз

Я собрал следующий простейший секундомер в 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, это та ситуация, когда изменяемое состояние оправдано, но после просмотра Уодлера "Монады для функционального программирования", мне все еще любопытно, как бы я мог обойтись без set!.

Я знаю, что для того, чтобы это работало, я должен добавить аргументы к своим функциям. Например, 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)])))

person Patrick Allo    schedule 22.04.2014    source источник


Ответы (3)


Что, вероятно, произойдет в следующей части вашей программы, так это то, что у вас будет цикл. Тогда этот цикл может быть функцией, которая принимает в качестве входных данных все текущее состояние, и когда вы хотите обновить его состояние, просто снова вызовите цикл с новым состоянием (конечно, вы также можете снова вызвать цикл с тем же точным состоянием). .

Упрощенный пример:

(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

Это, безусловно, меняет подход к вашему дизайну.

Этот подход сложнее использовать при разработке программ с графическим интерфейсом, потому что цикл событий часто мешает (или затрудняет) передачу значений от одного события к другому. Однако в Racket есть (педагогический, но все же очень хороший) big-bang, созданный именно для этого.

person Metaxal    schedule 22.04.2014
comment
Это, в самом строгом смысле, ответ на мой вопрос. (см. мое редактирование того, как я его использовал, объединив его с примером использования от @Greg Hendershot) - person Patrick Allo; 23.04.2014

В этом случае использование set! оправдано и его трудно избежать, потому что мы должны «запоминать» состояние между вызовами процедур. Что мы можем сделать, так это улучшить инкапсуляцию состояния, скрыв переменные, которые изменяются внутри процедуры, и используя диспетчер сообщений для доступа к процедурам, которые ссылаются на изменяемое состояние. Это очень похоже на то, что мы делаем с объектно-ориентированным программированием, но для его реализации требуется всего 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))

Эта техника называется «передача сообщений», подробнее о ней читайте в замечательном SICP книга.

person Óscar López    schedule 22.04.2014
comment
Я вернусь в SICP! - person Patrick Allo; 23.04.2014

Для такого простого примера я бы, вероятно, сделал то, что предложил @Metaxal.

Однако другой подход заключается в том, что вы можете явно определить состояние как struct:

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

Затем измените start, lap и 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.

Хорошая вещь в Racket (и Scheme) заключается в том, что вы можете выбрать любой подход из спектра, который, по вашему мнению, лучше всего подходит для стоящей перед вами проблемы и вашего вкуса - простой императив, императив ООП, чистый функционал.

person Greg Hendershott    schedule 22.04.2014
comment
Как оказалось, использование (read-line) было одной из самых простых вещей, о которых я не подумал, и это помешало мне сделать все правильно. - person Patrick Allo; 23.04.2014