Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Anything that is self-referential is hard. I remember when we covered proof-by-induction in HS math. There was a significant fraction of the class that did not believe a given proof was sufficient.

If you can't assure yourself of the correctness of a fairly simple recursive function that is given to you, then composing one yourself is going to be nigh impossible.

[edit]

From the article: Instead trust that it returns correct sum for all elements of an array a[i]

This is exactly the point where many people get hung up. They cannot proceed to reading the next line until they understand how the recursive call works. They are unwilling to trust that the function works as advertised, and cannot shift gears to continue past that. In my experience, telling them to "just trust it" and such seems to not help much.



> In my experience, telling them to "just trust it" and such seems to not help much.

Just call that recursive function something else. Give it some other name, and say that we must take it for granted that this other name is a function which is already debugged and tested by another team of programmers:

   (defun our-member (item list)
     (cond
       ((atom list) nil)
       ((eql item (car list)) list)
       (t (member item (cdr list)))))
Here, we rely on member, a standard function in the language already, and so there is no recursion.

We first convince ourselves that what we have works.

And then, final step: since what we have works, it means that our-member is a correct implementation of member, which means that the two are equivalent. Equivalent functions are substitutable. And so we can just perform that substitution: replace the member call with our-member.

By the way, it occurs to me that Hofstadter's Gödel, Escher and Bach might be a good source of ideas on how to deal with presenting and teaching self-referential topics.


Give it some other name, and say that we must take it for granted that this other name is a function which is already debugged and tested by another team of programmers

That seems like it's "begging the question" --- as in, "if there's already a function written to do what I want, why do I need it to write one to do it...?"


But that problem is present in virtually all homework in CS.

Someone already wrote a Celsius to Fahrenheit conversion function, a simple ray-tracer, a quicksort, a toy Lisp, a text-file-based simple inventory system, etc ...

We could make up a back story about this to give it real-world plausibility: yes, the function is already written, but it's in an expensive, proprietary third-party library which we cannot ship without paying royalty fees and being bogged down with various inconvenient restrictions.


But you don't call a library bubble sort function inside your own implementation of bubble sort!


My idea for teaching it is to start with just a function that calls the library function:

   (defun our-member (item list)
     (cond
       (t (member item list))))
Now everyone in the class should quickly agree that this function works. Next say "our goal is to write our own implementation of member, so we will handle one special case at a time until we don't need to call member anymore"

so step 1:

   (defun our-member (item list)
     (cond
       ((atom list) nil)     ;; if it's not a non-empty list, it can't be a list containing item
       (t (member item (cdr list)))))
step 2:

   (defun our-member (item list)
     (cond
       ((atom list) nil)            ;; if it's not a non-empty list, it can't be a list containing item
       ((eql (car list) item) list) ;; if the first element of the list is our item,
                                    ;; we have a match and return the whole list
       (t (member item list))))
step 3:

   (defun our-member (item list)
     (cond
       ((atom list) nil)              ;; if it's not a non-empty list, it can't be a list containing item
       ((eql (car list) item) list)   ;; if the first element of the list is our item,
                                      ;; we have a match and return the whole list
       (t (member item (cdr list))))) ;; we already know that the first item is not a match, so we can just run
                                      ;; member on the tail of the list
And now demonstrate that replacing member with our-member will still be correct.

[edit]

derp (atom nil) => T


I'd say that the function only handles input n-1, and your function needs to do n.


That is a brilliant idea. I'm going to borrow it next time I teach recursion.


>In my experience, telling them to "just trust it" and such seems to not help much.

I've sometimes got past the "leap of faith" step by replacing the recursive call with a call to a built-in. e.g.

  >>> sum([1, 2, 3, 4]) == 10
  True
  >>> def my_sum(xs):
  ...   if xs == []: return 0
  ...   else: return xs[0] + sum(xs[1:])
  >>> my_sum([1, 2, 3, 4]) == 10
It's often easier to accept this definition, because trusting that the built-in will work is comparatively simple. You then ask the audience what would happen if you replaced the call to the built-in with a recursive call.

Edit: It appears I've written an identical comment to my sibling.


This is exactly the point where many people get hung up. They cannot proceed to reading the next line until they understand how the recursive call works.

...which is why you should always teach recursion by starting with the base case and build on it with a recursive call, not the other way around. Step through an example execution of the algorithm with no recursive calls --- only the base case. Then step through with one or two levels of recursive calls, and they'll usually start seeing the pattern of how it works (and be able to generalise to n levels.)


Iteration is also self-referential. New iterations refer to prior values, and jump backwards to prior code.

I think the way to teach recursion to programmers who have mastered iteration is to teach the tricks for transforming loops into tail recursion: start by equating the self reference with the establishment of new values for the loop variables and the backward branch.

E.g. given a working dot product:

    double dot(double *a, double *b, size_t n)
    {
       double acc = 0;
       while (n-- != 0)
         acc += *a++ + *b++;
       return acc;
    }
We identify the loop variables: a, b, n, and acc. First, we can rewrite this using a form of "goto with parameters".

    #define dot_rec(label, na, nb, nn, nacc) { \
      /* capture argument evaluations into fresh temporary variables */ \
      double *tna = (na), *tnb = (nb); \
      size_t tnn = (nn); \
      double tnacc = (nacc); \
      /* store temporaries to loop variables, and jump backward */ \
      a = tna; b = tnb; n = tnn; acc = tnacc; \
      goto label; \
    }
 
    double dot(double *a, double *b, size_t n)
    {
       double acc = 0.0;
    again:
       if (n != 0)  /* we can now use (n > 0) since n isn't decremented! */ 
         dot_rec(again, a + 1, b + 1, n - 1, acc + *a * *b);
       return acc;
    }
Thus if n is not zero, we update a with a + 1, b with b + 1, n with n - 1, and the accumulator by adding a + b.

The dot_rec macro ensures that the expression * a is not affected by a receiving a + 1; it mimics the binding of argument expression values to fresh arguments.

From that we can go to recursion:

    double dot_rec(double *a, double *b, size_t n, double acc)
    {
       if (n > 0)
         return dot_rec(again, a + 1, b + 1, n - 1, acc + *a * *b);
       return acc;
    }

    double dot(double *a, double *b, size_t n)
    {
       return dot_rec(a, b, n, 0.0);
    }
Get people comfortable with mechanically converting iteration to recursion, even in cases where recursion isn't all that nice, and requires additional parameters and possibly the splitting of a function API into a non-recursive and recursive part.

Then go into non-tail recursion; recursion in which the parent actually does something with the return value other than just return it: those true divide-and-conquer problems.

Then mutual recursion among two or more functions, starting with something trivial like odd(n) and even(n) that mutually tail recurse.


> a working dot product:

    double dot(double *a, double *b, size_t n)
    {
       double acc = 0;
       while (n-- != 0)
         acc += *a++ + *b++;
       return acc;
    }
For a working dot product, I think you mean

         acc += *a++ + *b++;
to be

         acc += *a++ * *b++;
(That's how your recursive version does it, anyway.)


> I think you mean

Oh well, s/dot product/"dot sum"/ :) :) :)

> how your recursive version does it

See, everyone? Recursion irons out bugs!




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: