Search code examples
schemeracketsicpconscdr

What is the difference between `(mcons (mcons '() 25) 16)` and `(mcons 25 (mcons 16 `()))`


I am busy with Structure and Interpretation of Computer Programs exercise 2.18. Here we have to define a procedure reverse to reverse a list. It should do the following:

(reverse (list 1 4 9 16 25))
;; => (25 16 9 4 1)

I came up with the following definition:

(define (reverse list)
  (if (null? list) 
      list
      (cons (reverse (cdr list)) (car list))))
;; => (mcons (mcons (mcons (mcons (mcons '() 25) 16) 9) 4) 1).

Then in a solution In found something similar as follows:

(define (reverse items) 
  (if (null? (cdr items)) 
      items 
      (append (reverse (cdr items)) 
              (cons (car items) nil))))
;; => (mcons 25 (mcons 16 (mcons 9 (mcons 4 (mcons 1 '()))))).

There's a difference between append and cons here where I cannot put my finger on.

My question: what is the difference and why are the results not displayed as (25 16 9 4 1)?


Solution

  • Short answer: the first version of reverse is incorrectly building an improper list, the second version is inefficiently building a proper list. We can do better as long as we understand the difference between append and cons.

    append concatenates two lists. If we use it for just adding an element at the end of one list, we'll be doing a lot more of work than needed: we'll have to traverse the whole list each time just to put one last element (see: Schlemiel the Painter's algorithm). Hence a reverse implementation that uses append can be as bad as O(n^2) in complexity.

    On the other hand, cons adds a single element to the head of a list, for an O(n) complexity in the implementation of reverse. In general, in Scheme you should try to avoid using append to build a new output list, always prefer cons. Now let's see what's being returned by your algorithm using cons:

    (reverse '(1 2 3 4 5))
    => '(((((() . 5) . 4) . 3) . 2) . 1)
    

    Why is that? because to build a proper list with cons the second argument must be another proper list, but you're passing a single element. A correct implementation requires an accumulator parameter - and incidentally, this is more efficient, because it uses tail recursion, a concept that you should already be familiar with, as it was introduced in section 1.2 of the book. Try this:

    (define (reverse lst acc)
      (if (null? lst)
          acc
          (reverse (cdr lst) (cons (car lst) acc))))
    
    (reverse '(1 2 3 4 5) '())
    => '(5 4 3 2 1)
    

    For the last part of the question: the list is being displayed with mcons (the m means that the cons cells are mutable) because of the language you're using, try switching to #lang racket, which uses immutable cons cells by default, and will get printed as you expect.