
How the right syntax can help teach recursion - panic
http://akkartik.name/post/swamp
======
lhnz
Explaining recursion is a lot easier than writing recursive functions.

I can actually describe explicitly why I've occasionally found writing a
recursive function hard. In fact I expect a UI could be created that would
make it much much easier.

Basically, for ordinary functions I'm able to remember the values stored in
each of the variables. (I do not mean a real computation, but the pretend
computation I do at all times when programming.)

However, with recursive functions, I sometimes find myself having to remember
5 or more levels of these same variables. Therefore if I have 5 variables, I
might end up having to hold 25 values in my head; obviously it is less than
this since often there is some pattern to the values that aids in remembering
them.

Additionally, I need to flow the return values back through the functions
potentially performing extra computations on them; this can be extra confusing
if there were multiple entry-points at a particular recursion level of a
function. For example, what line and column number was the function called at,
and which block of a function was it within? And, do these values belong to
the 7th or 9th call of the function? Due to this I'm no longer able to rely on
my memory, and I don't get the cognitive benefits of associating computations
to place.

Another issue is that if you write some code which never matches its 'base
case' it never finishes computing. This is often difficult to debug, and it
can cause your machine to lock up.

Perhaps the solution might be:

\- A table to represent the values within a recursive function. With colour
coded cells that show which level of function a value comes from (and whether
it was an argument or return from a deeper level). Effectively, the idea is to
reassociate computation with 'place'. Method of Loci.

\- Some tools in order to troubleshoot the times in which they've
misprogrammed the 'base case'. For example, set a particular recursion depth
when developing and exit if this gets reached. Visualise separately how values
are advancing towards matching this 'base case'.

I could probably come up with more, but I need to think concretely again to do
so usefully; and I'd need to experiment with different recursive solutions to
find out whether I'm solving their individual problems.

~~~
spacehome
Maybe it's just that I'm not writing code as complicated as you, but 5 years
of school and 5 years of industry, I've never had to hold more than a single
level of a function in my head at once. My trick is to explicitly say what
contract the function fulfills, i.e. if the input has X property, then the
output has Y property. Then when you make the recursive call, you can just
assume your called function behaves as advertised. I thought this was the
whole point of recursion.

~~~
lhnz
What you're describing is how normal functions are generally written.

If there is no difference in complexity between writing a function that calls
itself and a function that just computes some data and returns it then surely
this discussion wouldn't be happening. I think it's happening for a reason so
I tried to articulate this.

I'm speculating that recursive solutions are more difficult for people to
write, because (1) whether a function is said to have worked or not could be
dependent on a call multiple levels deep from the original call, (2) the
immutable constant named `foo` you created within the function body might
contain different values at every deeper call (therefore it is no longer an
effective name to refer to 'one thing'), (3) the function calls each relate to
each other and likewise so do their values, but this mapping is generally not
expressed well within the code or within outputs you might debug (after each
function application, a value is received for which the context of how it was
produced is often lost.)

I'm probably speaking in too abstract a way to be clear. I think the tools
could be improved here; I was thinking about the problem earlier on and I
realised that generally I solve it by breaking the problems up into tiny
functions [0]. Unfortunately, there's a time cost to this approach, so I would
prefer it if debugging tools would instead allow you to annotate blocks of
code. They could then represent this information visually. This would help me.

I also think that good teaching is important. I remember that I used to be
utterly terrible at writing these functions: my main issue was often that I
didn't start by thinking about the 'base case' and the simplest inputs. That's
easy to get right.

I do think that my mind has never been very well suited to recursive
computation, however I believe I'm decent at understanding and problem-solving
around this. It's because I have to think carefully and tool myself when
dealing with recursive problems, that I'm able to describe the pitfalls. I
have never been the kind of person to compel myself into understanding
something merely by stating "this is easy".

[0]
[https://twitter.com/nouswaves/status/776384680652398592](https://twitter.com/nouswaves/status/776384680652398592)

~~~
drbawb
In a way I find recursive functions _reduce_ the complexity required to reason
about a loop. What made me grok recursion was realizing that it's just a more
explicitly defined loop:

\- You have to define an end condition. (Really this is no different than the
2nd clause of a C-style for() loop, or the expression evaluated by a `while()`
loop.)

\- You have to explicitly specify the inputs for each iteration of the loop
(e.g: explicitly declare what you need in the function signature, instead of
just using whatever variables happen to be in the parent scope as working
memory.)

\- It makes optimization concerns very quickly apparent, as you'll pretty
quickly blow the stack or start waiting for the heat-death of the universe.
The best heuristics I have for a quick optimization pass are: is it tail-
recursive, and does it needlessly recompute values?

Once I realized recursion is just another way to tell the compiler "repeat
this thing until [x] is true", it became a lot less mystical and much easier
to reason about.

I'd agree with OP to an extent that syntax did make a huge difference in how I
viewed recursion. I understood recursion on a theoretical level for a while,
but avoided it whenever possible because I too thought it was difficult to
reason about.

It wasn't until I learned Elixir that it "clicked" for me. Between pattern
matching and multiple function heads: Elixir just made recursion really easy
to think about. (Not having any traditional loop construct probably helped a
bit, too.)

~~~
lhnz
I agree with you that it is more explicit than looping: I actually think that
with complicated logic, loops suffer from even worse problems, and for simple
things most people just `filter`, `reduce` or `map`. That said, I do think the
debuggability could be improved.

------
yanickmartel
I think the best way to explain it is that a recursive function is a function
which should return either:

\- A 'base case' value

\- The result of a call to itself (could be multiple calls to itself also)

Basically when you call a recursive function, unless it hits the base case
(usually decided by an if condition), it will keep calling itself (often with
slightly different arguments each time) until it returns the base case.

Once the function returns the base case, the recursion will start to 'unwind'
\- In the unwind phase, you can use the return values of the previous
recursive calls to do more interesting stuff.

The best way to visualize recursion is to imagine that you have a stack of
function calls; each time a function calls itself, it creates a copy of itself
with new arguments and puts that copy on top of the stack - Then the program
pointer moves to the copy (but it keeps a reference of were it was in the
previous call) - And it keeps building up the stack with new copies of the
function until it reaches the base case (when the function finally returns a
concrete value).

The 'unwinding' phase is just when the functions start returning (after the
base case has been reached) and all the function 'copies' just get popped off
from the top of the stack one by one - Each time the program will continue
running from wherever it was in each call stack.

------
d--b
Wow, the guy really is teaching his students how to program using his side-
project language?!

Man, if I was the student, I would be pretty upset. There are plenty of
popular programming languages to learn. Each one has its own caveats. It's
hard enough to learn CaML, or C, or Lisp, but these are at least used in the
world!

Why would the student need to learn a programming language that no-one uses?
It's either that the teacher is using them as guinea pigs, which is bad
enough, or using them advertisement, which is even worse. All in all, he's
certainly not making them a favor!

~~~
dspillett
_> Why would the student need to learn a programming language that no-one
uses?_

The same reasons many examples in textbooks and documentation are in pseudo-
code. Sometimes you want to learn _general concepts_ and don't want
implementation oddities of _specific language(s)_.

Theory first then practice. That way you are more likely to end up with a
skillset you can easily transfer between languages/frameworks/platforms/other.

Essentially his language is the same as any other "made up" pseudo-code
syntax.

~~~
d--b
Well, yes and no. The syntax itself looks very verbose, and the language seems
to have a lot of idioms of its own.

------
noelwelsh
I disagree somewhat, but I don't have time to write a detailed answer. Instead
here's a quick sketch that is likely only intelligible if you have a fair bit
of PLT background.

The basic point is this: you don't need to teach "raw" recursion, just like
you don't teach control-flow using goto. Just like structured programming
introduced for/while/do loops to structure control flow, you can structure
recursive programs into a few major groups. The most important one is good old
structural recursion over algebraic data types. It's follows a very set
formula, so there isn't much opportunity to go wrong. It helps to have pattern
matching in your language to do this.

This is the approach taken by How to Design Programs
([http://htdp.org/](http://htdp.org/))

Example blog post you might read if you want to know more about the
theoretical background: [http://blog.sumtypeofway.com/an-introduction-to-
recursion-sc...](http://blog.sumtypeofway.com/an-introduction-to-recursion-
schemes/)

~~~
FeepingCreature
I think this depends on whether you're teaching down or up. I suspect both
work for different people.

(Down == "here's some abstract concept, here's some code that implements it in
the language's abstract model", Up == "Okay so everytime you call a function,
a new stackframe is created." That's the approach I personally prefer.)

I think the reason why recursion is so hard to understand is that programming
languages are incredibly misleading. You see "int a = 5;" and you assume that
there's a unique 'a' to which 5 has just been assigned, which is of course a
complete lie; 'a' is a _template_ for a memory location which is defined only
when the function is _called_. If you don't know this, recursion makes zero
sense. "What, now there's two a's??"

I should go do a blogpost on this.

~~~
noelwelsh
I think trying to reason about recursion in terms of stack frames is madness.
I find it quickly overwhelms my working memory. This is why we have the
patterns---so you don't have to reason on the machine level. It's similar[1]
to proof by induction, on the off chance that helps.

[1] By similar I mean exactly the same as.

~~~
FeepingCreature
I think _reasoning_ about recursion in terms of stackframes is silly. But
stackframes are useful for getting a handle on how recursion functions _at
all_.

~~~
noelwelsh
Ah, I see. Agreed with that.

------
shiro
Quoting the author:

    
    
        With Lisp it's always been non-trivial to know when a 
        function is tail-recursive. You can't just blindly count 
        parens, you have to try to "run" the function in your mind.
    

To me, Lisp code is just like a tree (well, sometimes DG) and it's statically
obvious where the leaves are---no need to "run" the function in mind. But
maybe it's just because I get used to it; once you learned, you forget what
you saw (or you didn't see) before.

~~~
riboflava
It can still be non-obvious with an optimizing compiler, or with non-Lisp.

I don't see the point in having beginning students think about tail call
optimized recursion vs normal recursion unless they're going through SICP. In
the end it can totally be a compiler thing. For instance, GCC can optimize
this:

    
    
        int factorial(int x) {
           if (x > 1) return x * factorial(x-1);
           else return 1;
        }
    

into this:

    
    
        int factorial(int x) {
           int result = 1;
           while (x > 1) result *= x--;
           return result;
        }
    

([http://ridiculousfish.com/blog/posts/will-it-
optimize.html](http://ridiculousfish.com/blog/posts/will-it-optimize.html))

~~~
shiro
When you're using TCO-guaranteed language like Scheme, what's important is you
recognize tail position, where TCO is guaranteed. Guaranteed TCO lets you
express, for example, state machine using mutually recursive functions. You do
see the tail calls as gotos and you heavily rely on that while you're coding.

In that regards, I feel that tail call "optimization" is misleading, since it
gives an impression that it's some kind of optional bonus the compiler gives
to you. The transformation such as your gcc example is, certainly, an
"optimization". If you get one, you're lucky; if you don't, fine, you can live
with it. Guaranteed tail call elimination is totally different---if you don't
have one, you change the way you write code.

(So, the blanket statement of "Lisp" might be inappropriate, for it's only a
subset of Lisps that has guaranteed TCO.)

------
jbb555
This just looks extra confusing to me

------
6DM
I feel like it would be easier to walk through how the machine deals with it
at an assembly level. I if you restrict to a really basic level of just
pushing, jumping, and linear execution. Recursion then just becomes executing
instructions, even if it means calling the same method you're already
executing in.

------
eutectic
I think the best way to understand recursion is in terms of induction. If
someone gave you the solution to your problem for different inputs, could you
compose them together to solve the problem for your current input?

Then you just have to show termination.

Thinking procedurally (i.e. in terms of 'unfolding' the call stack) is
unlikely to provide much insight.

------
bmm6o
It is a little interesting that it takes so few changes to the source to
rewrite the recursive call as a loop. But really the only advantage it has
over C (syntactically, anyway) is that it doesn't require a return at the end.
By which I mean, it's kind of a stretch to call this the "right" syntax.

------
qwertyuiop924
Funnily enough, that Lisp, guaranteed not to run anywhere in _this_ universe,
is _almost_ valid Scheme, under SRFI-49 or Wisp. The only thing you'd have
change is change def to define, and replace rest.x with (cdr x).

------
MayeulC
This is pretty interesting, and I am tempted to introduce this syntax in my
next C programs. Would anyone advise against it?

------
sn9
Just throw _The Little Schemer_ at them.

------
dschiptsov
Anything better than Haskell's?

