Gauche Devlog

< Unicode Case-mapping Support and Character Set Independence | Action items for 0.9.3 >


Two-argument reverse

Again I kept silence for over 4 months. I released 0.9.2 in August. Since then, quite a few changes are made into the development HEAD, and I'd like to start explaining them as I approach to release 0.9.3.

Some of the changes are small in the code, but can have great impact---meaning, it may change how you code in Gauche. However, they take a lot of words to explain; I need some more time.

For today, I pick a rather small topic to get up to the speed. It may even seem too trivial to mention, but somehow, it matters to me. Maybe because it is related to how I approach to a program.

★ ★ ★

A common pattern to transform a list is to tail-recurse the input with consing the results, and at the end reverse the result list to return:

  (let loop ([xs xs] [rs '()])
    (if (null? xs)
      (reverse rs)
      (loop (cdr xs) (cons (do-something (car xs)) rs))))

It is not necessarily a good style. Olin Shivers commented in his srfi-1 reference implementation that, this style is just trading execution stack for cons cells, and in stack-based system you should rather use a natural (non-tail) recursion:

  (let rec ([xs xs])
    (if (null? xs)
      (cons (do-something (car xs)) (rec (cdr xs)))))

In Gauche, the latter is just as fast as the former to a list up to about a thousand elements. However, if the input exceeds the limit and recursion gets too deep, Gauche starts moving the stack frames to the heap (a kind of stack GC), so it gets worse than using intermediate lists and reverse at the end. So I typically use cons-then-reverse idiom in Gauche.

Now, it is also common to write a function that adds the result to the existing pile of data. If we do non-tail recursive calls, it is trivial; I just replace () with the existing pile of data:

  (let rec ([xs xs])
    (if (null? xs)
      (cons (do-something (car xs)) (rec (cdr xs)))))

If we use tail-recursive call and reverse, however, we need to reverse the accumulated results, then append the existing pile of data. Srfi-1 has a procedure specifically for this pattern, append-reverse:

  (let loop ([xs xs] [rs '()])
    (if (null? xs)
      (append-reverse rs previous-results)
      (loop (cdr xs) (cons (do-something (car xs)) rs))))

There's nothing wrong with this code; the append-reverse procedure is meant to avoid unnecessary copying. Except that I always feel something awkward in the name: I feel it somewhat miss the point by thinking the operation as "reverse, then append".

Look at this:

(reverse xs)             == (fold cons '() xs)
(append-reverse xs init) == (fold cons init xs)

It looks like reverse is really a special case of append-reverse with the init to be (). Thinking the operation as "reverse, then append" is actually upside-down. It may be more natural to think the operation represented by append-reverse as a fundamental one, and reverse is a shorthand for the special case.

Then, wouldn't it be reasonable to extend reverse this way?

(define (reverse xs :optional (tail '()))
  (fold cons tail xs))

If you stick to the idea that reverse is an operation to reverse the order of the given list, this extension seems contrived. However, how about thinking reverse as a list constructor, which happens to take initial content from xs but in the reverse order?

Actually, 0.9.2's reverse is already extended to take an optional tail argument, though it's not documented.

Tradition has great inertia, so I don't expect this extension gets widely accepted; after all, you can use srfi-1's append-reverse if you want this functionality. But I did get a little piece of mind with this extension.

Tags: reverse, srfi-1, append-reverse

Post a comment