[PRL] Accumulator for container capacity
Jesse A. Tov
tov at ccs.neu.edu
Fri Sep 23 17:41:21 EDT 2005
Since Matthias asked in a different thread about accumulators and
monads, I thought it might be worth joining the two. Here's a solution
using the state monad in a straightforward manner: The number of
violations is carried by the implicit state, and there's one update to
the state that corresponds to where Matthias used set!. After that,
there's a monadic version using Will's visitor/fold, and a second that
uses the same container visitor/fold but avoids the double traversal by
using foldl lifted into the state monad. Finally, there's an
"inside-out" version that keeps track of weight/capacity with the
implicit state and explicitly returns the number of violations.
; Abstract over monads -- a monad is pair of operations, unit and bind:
(define-struct monad (unit bind))
;; Given that pair of operations, there are several other
;; monad operators we might want to define:
;; (sequence monad): list(monad(x)) -> monad(list(x))
(define (sequence monad)
(lambda (lom)
(cond
[(null? lom) ((monad-unit monad) '())]
[else ((monad-bind monad)
(car lom)
(lambda (v)
((monad-bind monad)
((sequence monad) (cdr lom))
(lambda (vs)
((monad-unit monad) (cons v vs))))))])))
;; (map-m monad): list(a) (a -> monad(b)) -> monad(list(b))
(define (map-m monad)
(lambda (f lov)
((sequence monad) (map f lov))))
;; (foldl-m monad): (a b -> monad(b)) b list(a) -> monad(b)
(define (foldl-m monad)
(lambda (f z lov)
(cond
[(null? lov) ((monad-unit monad) z)]
[else ((monad-bind monad)
(f z (car lov))
(lambda (z)
((foldl-m monad) f z (cdr lov))))])))
;; Example: the list monad
(define (list-unit x)
(list x))
(define (list-bind m k)
(apply append (map k m)))
(define list-monad
(make-monad list-unit list-bind))
;; The State Monad
;; Every step/action in the state monad is a function from a state to a
;; pair of result and state. To run the state monad, we apply it to an
;; initial state:
;; sm-run: sm(s, r) s -> (r . s)
(define (sm-run sm s0)
(sm s0))
;; Usually we'll use one of these instead:
;; sm-exec: sm(s, r) s -> s
;; sm-eval: sm(s, r) s -> r
(define (sm-eval sm s0)
(car (sm-run sm s0)))
(define (sm-exec sm s0)
(cdr (sm-run sm s0)))
;; sm-unit: r -> sm(s, r)
(define (sm-unit a)
(lambda (s)
(cons a s)))
;; sm-bind: sm(s, r1) (r1 -> sm(s, r2)) -> sm(s, r2)
(define (sm-bind m k)
(lambda (s)
(let ((r0 (sm-run m s)))
(sm-run (k (car r0)) (cdr r0)))))
(define sm (make-monad sm-unit sm-bind))
;; State monad has two additional primitive operations:
;; sm-get: sm(s, s); to read the state
;; sm-put: s -> sm(s, '()); to write the state
(define (sm-get)
(lambda (s)
(cons s s)))
(define (sm-put s)
(lambda (_)
(cons '() s)))
;;;;; Here lies the abstraction barrier.
;; Map f over the state, return '().
;; sm-modify: (s -> s) -> sm(s, '())
(define (sm-modify f)
(sm-bind
(sm-get)
(lambda (s)
(sm-put (f s)))))
;; Okay, now we're ready.
;; From MF:
(define-struct Container (contents capacity))
(define-struct Simple (name weight))
(define c-1
(make-Container (list
(make-Simple "apple" 1))
1))
(define c0
(make-Container (list
(make-Simple "pencil" 1)
c-1
(make-Simple "orange" 1))
1))
(define c1
(make-Container (list
(make-Simple "apple" 1)
c0
(make-Simple "orange" 1)
(make-Simple "kiwi" 1))
5))
(define (test-violations violations)
(list
(= (violations c-1) 0)
(= (violations c0) 1)
(= (violations c1) 2)))
;; Okay, here's something like Mitch's solution. Each recursive call to
;; the auxiliary function returns a list containing the weight and
violations
;; in that subcontainer.
(define (violations-direct c)
;; item -> (list weight violations)
(define (w-and-v-item c)
(cond
[(Simple? c) (list (Simple-weight c) 0)]
[(Container? c) (w-and-v-container c)]))
;; container -> (list weight violations)
(define (w-and-v-container c)
(let
((nest (apply map + (map w-and-v-item (Container-contents c)))))
(cond
[(> (car nest) (Container-capacity c))
(list (car nest) (+ 1 (cadr nest)))]
[else nest])))
(cadr (w-and-v-item c)))
'testing-violations-direct
(test-violations violations-direct)
;; Here's a pretty straightforward translation of that to the
;; state monad:
(define (violations-monadic c)
;; item -> sm(violations, weight)
(define (weight-item c)
(cond
[(Simple? c) (sm-unit (Simple-weight c))]
[(Container? c) (weight-container c)]))
;; container -> sm(violations, weight)
(define (weight-container c)
(sm-bind
((map-m sm) weight-item (Container-contents c))
(lambda (weights)
(let ((weight (foldl + 0 weights)))
(sm-bind
(if (> weight (Container-capacity c))
(sm-modify add1)
(sm-unit '()))
(lambda (_)
(sm-unit weight)))))))
(sm-exec (weight-item c) 0))
'testing-violations-monadic
(test-violations violations-monadic)
;; Will defines a fold over container trees, something like:
(define (make-item-visitor visit-simple visit-container)
(define (visitor c)
(cond
[(Simple? c) (visit-simple (Simple-name c) (Simple-weight c))]
[(Container? c)
(visit-container (map visitor (Container-contents c))
(Container-capacity c))]))
visitor)
;; We can the state monad with a fold:
(define (violations-monadic-visitor c)
;; name weight -> sm(violations, weight)
(define (visit-simple name weight)
(sm-unit weight))
;; list-of(sm(violations, weight)) capacity -> sm(violations, weight)
(define (visit-container m-weights capacity)
(sm-bind
((sequence sm) m-weights)
(lambda (weights)
(let ((weight (foldl + 0 weights)))
(sm-bind
(if (> weight capacity)
(sm-modify add1)
(sm-unit '()))
(lambda (_)
(sm-unit weight)))))))
(sm-exec
((make-item-visitor visit-simple visit-container) c)
0))
'testing-violations-monadic-visitor
(test-violations violations-monadic-visitor)
;; If we really want "one pass", we might get what we wish for (eww!):
(define (violations-monadic-visitor-1pass c)
;; name weight -> sm(violations, weight)
(define (visit-simple name weight)
(sm-unit weight))
;; list-of(sm(violations, weight)) capacity -> sm(violations, weight)
(define (visit-container m-weights capacity)
(sm-bind
((foldl-m sm)
(lambda (weight-so-far m-weight)
(sm-bind
m-weight
(lambda (weight)
(sm-unit (+ weight-so-far weight)))))
0 m-weights)
(lambda (weight)
(sm-bind
(if (> weight capacity)
(sm-modify add1)
(sm-unit '()))
(lambda (_)
(sm-unit weight))))))
(sm-exec
((make-item-visitor visit-simple visit-container) c)
0))
'testing-violations-monadic-visitor-1pass
(test-violations violations-monadic-visitor-1pass)
;; We can switch the state and the result -- that is, keep track of
;; capacity/weight in the state and explicity return the violations.
;; To do this, we're probably going to want to use the state as a
;; stack:
(define (sm-push v)
(sm-bind
(sm-get)
(lambda (s)
(sm-put (cons v s)))))
(define (sm-pop)
(sm-bind
(sm-get)
(lambda (s)
(sm-bind
(sm-put (cdr s))
(lambda (_)
(sm-unit (car s)))))))
(define (violations-monadic-visitor-inside-out c)
(define (visit-simple name weight)
(sm-bind
(sm-modify
(lambda (stack)
(map (lambda (capacity) (- capacity weight)) stack)))
(lambda (_)
(sm-unit 0))))
(define (visit-container m-violations capacity)
(sm-bind
(sm-push capacity)
(lambda (_)
(sm-bind
((sequence sm) m-violations)
(lambda (violations-list)
(let ((violations (foldl + 0 violations-list)))
(sm-bind
(sm-pop)
(lambda (capacity-remaining)
(sm-unit (if (< capacity-remaining 0)
(+ 1 violations)
violations))))))))))
(sm-eval
((make-item-visitor visit-simple visit-container) c)
'()))
'testing-violations-monadic-visitor-inside-out
(test-violations violations-monadic-visitor-inside-out)
;; Jesse
More information about the PRL
mailing list