git @ Cat's Eye Technologies The-Dossier / master article / Nested-Modal-Transducers / nested-modal-transducers.scm
master

Tree @master (Download .tar.gz)

nested-modal-transducers.scm @masterraw · history · blame

; SPDX-FileCopyrightText: In 2019, Chris Pressey, the original author of this work, placed it into the public domain.
; SPDX-License-Identifier: Unlicense
; For more information, please refer to <https://unlicense.org/>

;
; Runnable example code, in R5RS Scheme, to accompany the article
; about Nested Modal Transducer Assemblages.
;
; Example usage: install Chicken Scheme, then run
;     csi -q -b nested-modal-transducers.scm
; to run all the tests.  All tests passed if the output is only `()`'s.
;

(define expect
  (lambda (pairs)
    (if (null? pairs)
      '()
      (let* ((pair (car pairs))
             (fst (car pair))
             (snd (cdr pair)))
        (if (equal? fst snd)
          (expect (cdr pairs))
          pair)))))

;
; Purely functional definition of a simple transducer.
;

(define light-transducer
  (lambda (mode input)
    (let* ((transition (list mode input)))
      (cond
        ((equal? transition '(on turn-off))
          (list 'off '()))
        ((equal? transition '(off turn-on))
          (list 'on '(ring-bell)))
        (else
          (list mode '()))))))

;
; Purely functional test harness for transducers:
; Determine what state and outputs it will produce, given a sequence of inputs.
; You can think of it as having a type like:
;
;     rehearse :: Transducer -> State -> [Input] -> (State, [Output])
;

(define rehearse
  (lambda (t state inputs)
    (if (null? inputs)
      (list state '())
      (let* ((input (car inputs))
             (result1 (t state input))
             (state1 (car result1))
             (outputs1 (cadr result1))
             (result2 (rehearse t (car result1) (cdr inputs)))
             (state2 (car result2))
             (outputs2 (cadr result2)))
        (list state2 (append outputs1 outputs2))))))



(display (expect (list
  (cons
    (rehearse light-transducer 'on '(turn-off))
    '(off ())
  )
  (cons
    (rehearse light-transducer 'off '(turn-off))
    '(off ())
  )
  (cons
    (rehearse light-transducer 'off '(turn-on turn-on turn-off))
    '(off (ring-bell))
  )
  (cons
    (rehearse light-transducer 'on '(turn-on turn-on turn-off))
    '(off ())
  )
)))
(newline)


;
; ---- ---- ---- ----
;

(define combine-transducers
  (lambda (ta tb)
    (lambda (state input)
      (let* ((state-a    (car state))
             (result-a   (ta state-a input))
             (newstate-a (car result-a))
             (outputs-a  (cadr result-a))
             (state-b    (cdr state))
             (result-b   (tb state-b input))
             (newstate-b (car result-b))
             (outputs-b  (cadr result-b)))
        (list (cons newstate-a newstate-b) (append outputs-a outputs-b))))))

(define two-light-transducer (combine-transducers light-transducer light-transducer))

(display (expect (list
  (cons
    (rehearse two-light-transducer '(on . off) '(turn-off))
    '((off . off) ())
  )
  (cons
    (rehearse two-light-transducer '(on . off) '(turn-off turn-on))
    '((on . on) (ring-bell ring-bell))
  )
)))
(newline)

;
; ---- ---- ---- ----
;

(define counting-light-transducer
  (lambda (config input)
    (let* ((mode       (car config))
           (count      (cadr config))
           (transition (list mode input)))
      (cond
        ((equal? transition '(on turn-off))
          (list (list 'off count) '()))
        ((equal? transition '(off turn-on))
          (list (list 'on (+ count 1)) '(ring-bell)))
        (else
          (list config '()))))))

(display (expect (list
  (cons
    (rehearse counting-light-transducer '(off 0) '(turn-on))
    '((on 1) (ring-bell))
  )
  (cons
    (rehearse counting-light-transducer '(off 0) '(turn-on turn-on))
    '((on 1) (ring-bell))
  )
  (cons
    (rehearse counting-light-transducer '(off 0) '(turn-on turn-on turn-off))
    '((off 1) (ring-bell))
  )
  (cons
    (rehearse counting-light-transducer '(off 0) '(turn-on turn-on turn-off turn-on))
    '((on 2) (ring-bell ring-bell))
  )
)))
(newline)

;
; Nested state machine.  The light is now in a room, behind a door.
; It can only be turned on or off when the door is open.
;

(define door-transducer
  (lambda (config input)
    (let* ((mode         (car config))
           (light-config (cadr config))
           (transition   (list mode input)))
      (cond
        ((equal? transition '(closed open))
          (list (list 'opened light-config) '()))
        ((equal? transition '(opened close))
          (list (list 'closed light-config) '()))
        ((equal? mode 'opened)
          (let* ((inner-result     (counting-light-transducer light-config input))
                 (new-light-config (car inner-result))
                 (light-outputs    (cadr inner-result)))
            (list (list mode new-light-config) light-outputs)))
        (else
          (list config '()))))))

(display (expect (list
  (cons
    (rehearse door-transducer '(closed (off 0)) '(open))
    '((opened (off 0)) ())
  )
  (cons
    (rehearse door-transducer '(closed (off 0)) '(turn-on))
    '((closed (off 0)) ())
  )
  (cons
    (rehearse door-transducer '(closed (off 0)) '(open turn-on close))
    '((closed (on 1)) (ring-bell))
  )
)))
(newline)

;
; Array of orthogonal regions - a list of lights are behind a barn door.
;

(define transduce-all
  (lambda (t input configs acc)
    (if (null? configs)
      (list (reverse (car acc)) (cadr acc))
      (let* ((config        (car configs))
             (rest-configs  (cdr configs))
             (acc-configs   (car acc))
             (acc-outputs   (cadr acc))
             (result        (t config input))
             (new-config    (car result))
             (these-outputs (cadr result))
             (new-acc       (list (cons new-config acc-configs) (append these-outputs acc-outputs))))
        (transduce-all t input rest-configs new-acc)))))

(define barn-transducer
  (lambda (config input)
    (let* ((mode          (car config))
           (light-configs (cadr config))
           (transition    (list mode input)))
      (cond
        ((equal? transition '(closed open))
          (list (list 'opened light-configs) '()))
        ((equal? transition '(opened close))
          (list (list 'closed light-configs) '()))
        ((equal? mode 'opened)
          (let* ((inner-results     (transduce-all counting-light-transducer input light-configs '(() ())))
                 (new-light-configs (car inner-results))
                 (light-outputs     (cadr inner-results)))
            (list (list mode new-light-configs) light-outputs)))
        (else
          (list config '()))))))


(display (expect (list
  (cons
    (rehearse barn-transducer '(closed  ((off 0) (on 0)) ) '(open))
    '( (opened  ((off 0) (on 0)) ) ())
  )
  (cons
    (rehearse barn-transducer '(closed  ((off 0) (on 0)) ) '(turn-on))
    '( (closed  ((off 0) (on 0)) ) ())
  )
  (cons
    (rehearse barn-transducer '(closed  ((off 0) (on 0)) ) '(open turn-on close))
    '( (closed  ((on 1) (on 0)) ) (ring-bell))
  )
  (cons
    (rehearse barn-transducer '(closed  ((off 0) (off 0)) ) '(open turn-on close))
    '( (closed  ((on 1) (on 1)) ) (ring-bell ring-bell))
  )
)))
(newline)


;
; Entry and exit actions
;
; Like the article says, we don't pretend to have a good solution, we only
; want to show that it is possible.
;
; door-transducer-2 is the same as door-transducer except that the
; counting-light-transducer nested within it, is decorated with
; add-entry-exit-outputs.
;

(define add-entry-exit-outputs
  (lambda (t config input)
    (let* ((old-mode      (car config))
           (result        (t config input))
           (new-config    (car result))
           (new-mode      (car new-config))
           (new-data      (cadr new-config))
           (outputs       (cadr result))
           (exit-outputs  (if (equal? old-mode 'off) '(buzz-buzzer) '()))
           (entry-outputs (if (equal? new-mode 'closed) '(blow-horn) '()))
           (new-outputs   (append exit-outputs outputs entry-outputs)))
      (list new-config new-outputs))))

(define door-transducer-2
  (lambda (config input)
    (let* ((mode         (car config))
           (light-config (cadr config))
           (transition   (list mode input)))
      (cond
        ((equal? transition '(closed open))
          (list (list 'opened light-config) '()))
        ((equal? transition '(opened close))
          (list (list 'closed light-config) '()))
        ((equal? mode 'opened)
          (let* ((inner-result     (add-entry-exit-outputs counting-light-transducer light-config input))
                 (new-light-config (car inner-result))
                 (light-outputs    (cadr inner-result)))
            (list (list mode new-light-config) light-outputs)))
        (else
          (list config '()))))))

(define deco-door-transducer
  (lambda (config input)
    (add-entry-exit-outputs door-transducer-2 config input)))


(display (expect (list
  (cons
    (rehearse deco-door-transducer '(closed (off 0)) '(open))
    '((opened (off 0)) ())
  )
  (cons
    (rehearse deco-door-transducer '(closed (off 0)) '(turn-on))
    '((closed (off 0)) (blow-horn))
  )
  (cons
    (rehearse deco-door-transducer '(closed (off 0)) '(open turn-on close))
    '((closed (on 1)) (buzz-buzzer ring-bell blow-horn))
  )
)))
(newline)


;
; Synthesized events
;

(define make-gui-input-synthesizing-transducer
  (lambda (t)
    (lambda (config input)
      (let* ((mode (car config)))
        (cond
          ((and (equal? mode 'mouse-down) (list? input) (equal? (car input) 'mouse-move))
            (t config (list 'drag (cadr input) (caddr input))))
          (else
            (t config input)))))))

(define base-gui-transducer
  (lambda (config input)
    (let* ((mode       (car config))
           (x          (cadr config))
           (y          (caddr config))
           (transition (list mode input)))
      (cond
        ((equal? transition '(mouse-down mouse-release))
          (list (list 'mouse-up x y) '()))
        ((equal? transition '(mouse-up mouse-press))
          (list (list 'mouse-down x y) (list (list 'show-click x y))))
        ((equal? (car input) 'mouse-move)
          (let* ((new-x (cadr input)) (new-y (caddr input)))
            (list (list mode new-x new-y) '())))
        ((equal? (car input) 'drag)
          (let* ((new-x (cadr input)) (new-y (caddr input)))
            (list (list mode new-x new-y) (list (list 'show-hand new-x new-y)))))
        (else
          (list (list mode x y) '()))))))

(define gui-transducer (make-gui-input-synthesizing-transducer base-gui-transducer))


(display (expect (list
  (cons
    (rehearse gui-transducer '(mouse-up 0 0) '((mouse-move 10 10) mouse-press mouse-release))
    '((mouse-up 10 10) ((show-click 10 10)))
  )
  (cons
    (rehearse gui-transducer '(mouse-up 0 0) '(mouse-press (mouse-move 10 10) mouse-release))
    '((mouse-up 10 10) ((show-click 0 0) (show-hand 10 10)))
  )
)))
(newline)