Using Tracing to help your brain start thinking recursively

Being able to conceptualize computations recursively is an important and powerful skill for any computer scientist, regardless of the language in which you ultimately decide to implement your algorithmic concept. Recursive thinking essentially requires you to take your brain to a new level, being able to visualize not just a single subprogram call, but at entire runtime stack of subprograms building up and unraveling as the program runs. Obviously this ability is not just good for Scheme or even recursion itself, it is a fundamental skill that is essential in computer science.

This would be all well and good, if this level of runtime visualization were easy to...well, visualize! Even though you'll get very good at this fast, getting started can be tough: who was it that called who...and with what parameters? And things get even worse for recursion: sure, I know that it was me that called myself...but with what parameters on each instantiation of the call, how many levels of previous calls are waiting, and what return values are passed back by each instantiation?

DrRacket has a decent little debugger included, but it doesn't help (at least not me!) very much with its "tracing" functionality. Arrows going all over the place, whew! Tough to see how that's helpful. What I need is a clean way to trace (possibly recursive) calls of some function I'm interested in. Fortunately, the basic scheme package provided by DrRacket can help out. Here's how:

Example:

Suppose we want to create a nifty mega-delete function. It takes in a (potentially deeply nest) list and a target atom, and returns that same list with all occurrences of the target atom deleted, wherever it is in the complex list structure. So you take a breath and whip up your function. Maybe something like:

;Deep delete function.  Give it a deeply nested list plus a target element, and
; deletes every occurrence of target anywhere in the nested list structure.
(define d-del
  (lambda (alist target)
           ; first, if you ask me to d-del empty list, answer is easy: empty
    (cond ((null? alist) '())  
          ;; ok, not empty. So now if car is a list, then answer= d-del of car
          ;; consed back onto d-del of the remaining list.
          ((list? (car alist))
           (cons (d-del (car alist) target) (d-del (cdr alist) target)))
          ;; ok, car is not a list. So it must be an atom. If that atom is NOT our target
          ;; then answer is to cons it onto whatever we get back from d-del on the rest.
          ((not (eq? (car alist) target))
           (cons (car alist) (d-del (cdr alist) target)))
          ;; ok, if not any of above, then car must be equal to target and we want to
          ;; delete it.  Meaning the answer is just d-del on the cdr.
          (else (d-del (cdr alist) target)))))

;; Define silly old list to test with
(define testlist
  '(a f 3 (y (a d j (s e a)) p q a d) m a d (foot flo (3 4) fargit a bout it)))
  
Ok, looking good. Now let's give it a quick test run:
Welcome to DrRacket, version 6.4 [3m].
Language: Pretty Big; memory limit: 128 MB.
> testlist
(a f 3 (y (a d j (s e a)) p q a d) m a d (foot flo (3 4) fargit a bout it))
> (d-del testlist 'a)
(f 3 (y (d j (s e)) p q d) m d (foot flo (3 4) fargit bout it))
>

Ok, that's looking super too. But holy cow, how does this thing work? My brain is spinning with all the recursion! I sure wish I could get some help visualizing what's going on! Well you can! let's fire up scheme's tracing facility: Add this line to the top of you code; it imports racket's trace facility to use:
(require racket/trace) 
Now "run" the code to re-process your code, and now go ahead and turn on tracing for the function(s) you want; in this case, we'll trace d-del and run it. Check this out:
Welcome to DrRacket, version 6.4 [3m].
Language: Pretty Big; memory limit: 128 MB.
> testlist
(a (f 3 y) (a d j (s e a) b (d a)))
> (d-del testlist 'a)
((f 3 y) (d j (s e) b (d)))
> (trace d-del)
> (d-del testlist 'a)

>(d-del '(a (f 3 y) (a d j (s e a) b (d a))) 'a)
>(d-del '((f 3 y) (a d j (s e a) b (d a))) 'a)
> (d-del '(f 3 y) 'a)
> >(d-del '(3 y) 'a)
> > (d-del '(y) 'a)
> > >(d-del '() 'a)
< < <'()
< < '(y)
< <'(3 y)
< '(f 3 y)
> (d-del '((a d j (s e a) b (d a))) 'a)
> >(d-del '(a d j (s e a) b (d a)) 'a)
> >(d-del '(d j (s e a) b (d a)) 'a)
> > (d-del '(j (s e a) b (d a)) 'a)
> > >(d-del '((s e a) b (d a)) 'a)
> > > (d-del '(s e a) 'a)
> > > >(d-del '(e a) 'a)
> > > > (d-del '(a) 'a)
> > > > (d-del '() 'a)
< < < < '()
< < < <'(e)
< < < '(s e)
> > > (d-del '(b (d a)) 'a)
> > > >(d-del '((d a)) 'a)
> > > > (d-del '(d a) 'a)
> > > > >(d-del '(a) 'a)
> > > > >(d-del '() 'a)
< < < < <'()
< < < < '(d)
> > > > (d-del '() 'a)
< < < < '()
< < < <'((d))
< < < '(b (d))
< < <'((s e) b (d))
< < '(j (s e) b (d))
< <'(d j (s e) b (d))
> >(d-del '() 'a)
< <'()
< '((d j (s e) b (d)))
<'((f 3 y) (d j (s e) b (d)))
((f 3 y) (d j (s e) b (d)))
>

Wow! Cool, you can see the whole call stack unfolding!

So note a couple of things:

Walking through a couple of these traces can be really helpful to understanding what's going on...to help you get your brain thinking recursively. Plus it can be quite helpful in debugging, if you're just not getting back the value you expected. Obviously you can turn tracing on for individual functions; you can trace some and not others.

To turn tracing off, simply do:

 (untrace d-del) 

Have fun!