A list builder or list accumulator
Wow, so sorry I missed seeing this question! (Normally I get emails when someone posts something tagged with scheme, but didn't see this one till now. :-()

So, the idiomatic way to build a list in Scheme is to cons elements to the front each time, then reverse the whole list at the end. This is still O(n) on the number of items to insert, so you're no worse off compared to the set-cdr! solution.

However, for this situation, a better solution (IMHO) in your case is actually to use unfold, which takes any given object (not necessarily a list) and results in a list. This is perfect for your situation because you are processing variable numbers of items in your input list at each step, which would make it a poor fit for map, fold, and the like (which processes exactly one item at each step).

Here's an example (require SRFIs 1, 8, and 60):

(define (decode-utf8 bytes)
  (define (invalid byte)
    (list 'invalid byte))
  (define (non-shortest val)
    (list 'non-shortest val))
  (define (end-of-input val)
    (list 'end-of-input val))

  (define (decode-start bytes)
    (define byte (car bytes))
    (define rest (cdr bytes))
    (cond ((&lt; byte #x80) (values (integer-&gt;char byte) rest))
          ((&lt; byte #xC0) (values (invalid byte) rest))
          ((&lt; byte #xE0) (decode-rest 1 #x80 (logand byte #x1F) rest))
          ((&lt; byte #xF0) (decode-rest 2 #x800 (logand byte #x0F) rest))
          ((&lt; byte #xF8) (decode-rest 3 #x10000 (logand byte #x07) rest))
          (else (values (invalid byte) rest))))
  (define (decode-rest count minval val bytes)
    (cond ((zero? count) (values ((if (&lt; val minval)
                                      non-shortest
                                      integer-&gt;char) val) bytes))
          ((null? bytes) (values (end-of-input val) bytes))
          (else
            (let ((byte (car bytes))
                  (rest (cdr bytes)))
              (cond ((&lt; byte #x80) (values (invalid byte) rest))
                    ((&lt; byte #xC0)
                     (decode-rest (- count 1) minval
                                  (+ (ash val 6) (logand byte #x3F))
                                  rest))
                    (else (values (invalid byte) rest)))))))

  (define (decode bytes)
    (receive (value rest) (decode-start bytes)
      value))
  (define (next bytes)
    (receive (value rest) (decode-start bytes)
      rest))

  (unfold null? decode next bytes))


The real action happens in decode-start and decode-rest, of course; both functions return two values:


the character decoded (or an error)
a reference to the next byte to process


The decode function extracts the first value, while next extracts the second.



If you still want to use a list builder, that's fine too. Here's a simple version: it builds the list when given no arguments, and otherwise appends the arguments given:

(define (make-list-builder)
  (define cur '())
  (case-lambda
   (() (reverse cur))
   ((item) (set! cur (cons item cur)))    ; redundant
   (items (set! cur (append-reverse items cur)))))


(where append-reverse comes from SRFI 1). Note that append-reverse with only one item is identical to cons, so the cons line above is redundant; it just demonstrates how you would do it if you want to add items one by one.

If you want to be able to send messages like in your version, here's how one might implement it:

(define (make-list-builder)
  (define cur '())
  (lambda (msg . args)
    (case msg
      ((add) (set! cur (append-reverse args cur)))
      ((get) (reverse cur))
      ((clear) (set! cur '())))))


In both cases, we keep track of a single variable only, not the start and end of list, because we are prepending elements rather than appending them.



There is still a valid use for the start-and-end approach you had, though: it's a common approach for implementing a queue. You enqueue by using the end reference, set-cdr!ing the new value in, then resetting the end reference; and of course, you dequeue by using the start reference. Here's an example (zero arguments means dequeue; otherwise enqueue the given arguments):

(define (make-queue)
  (define start (list 'queue-head))
  (define end start)
  (case-lambda
   (() (when (null? (cdr start))
         (error "empty queue"))
       (let ((result (cadr start)))
         (set-cdr! start (cddr start))
         (when (null? (cdr start))
           (set! end start))
         result))
   (items (set-cdr! end items)
          (set! end (last-pair end)))))


(here, last-pair comes from SRFI 1).