How to Get Better at Recursion
notes.eatonphil.com
notes.eatonphil.com
As you enter you are creating elements (state) and store it on the stack and as you exit you are reducing elements.
When thinking this way you can notice a lot of interesting points about recursion.
For example, a simple way to test if you can get away with tail recursion is if you can reduce elements as you enter. It means you don't have to store elements as you enter -- no need for data structure on the stack == no need for actual recursion.
Another observation to remember is you can write that as an actual stack and implement any recursive problem (for example tree traversal) without recursion. Your data structure contains the state that you need.
I would sometimes write recursive solution first and then see if it might be better off rewritten to use a separate stack and loop.
Yet another important observation is that on a program stack you don't normally access anything than top of the stack. But for your algorithm having access to all previous levels might be useful. That's where writing it as a separate data structure might be advantageous.
Practicing working with true recursion would involve algorithms that are not tail-recursive, such as tree traversal.
Just read SICP. You can thank me later.
Putting a function call in a loop maybe does that more clearly though
https://github.com/bmitc/the-little-schemer
I'm about to finish chapter 10, the last chapter and the interpreter section, probably tonight.
Another suggestion for getting comfortable with recursion is the Coursera course by Dan Grossman, Programming Languages.
For an example, see https://github.com/tmoertel/practice/blob/master/dailycoding...
I did so in one of my side projects that I have recently released as an iOS and macOS puzzle game named "Recursive" where recursion plays a central role and allows solving levels with very short programs [0].
Also, I may be biased, but I think that players can improve their mastery of recursion using the game.
If you'd like you can contact me and I will gladly send you a promo code (contact email at website).
[0] https://www.kidori.com/games/recursive/
(edited to fix typos)
I learned recursion in University, it's undeniably a foundational piece of Computer Science theory. I use recursion when I have to do interview problems. And I think that's about it.
The number of times I actually used recursion in production was maybe twice. And one of those times the PR was rejected. In my experience the overhead of understanding the code outweighs its terseness. This is also how I sometimes feel about macros in clojure. The cons outweigh the pros.
- Traversed a directory hierarchy to compute the total size or other statistics about its contents?
- Searched for a DOM element on a web page matching certain criteria without using third-party libraries or helper functions like querySelector()?
- Implemented a hierarchical navigation structure in a user interface?
- Work on parts of a compiler/interpreter or other code that has to deal with formal language?
- Written a parser for a file format that contains arbitrarily nested data structures (e.g. HTML/JSON)?
- Done anything with trees?
- Solved a problem by breaking it down into smaller versions of the same problem, solving those, and then combining the results?
I know that for lots of programming tasks recursion is not needed, but I'm genuinely curious as to what kind of software you work on where you've never encountered a problem that requires a recursive solution?
So far, I've had write recursive code just once to convert SOAP requests/response to REST request/response on the fly so that the test cases that were written using SOAP client could be reused to test REST end point as well.
To state the obvious, the chances of encountering recursive code depends on the domain. In my experience though a typical CRUD business app won't require a recursion as you pointed out. The examples you sited are mostly encountered in a "framework" code (e.g., Spring, some UI framework) and as is typically the case the number of developers "using" a framework is an order of magnitude more than those who implement/maintain them. Same is the case with compiler. On the other hand, those who deal with the lower level system code (OS Kernel, RTOS etc.,) shun recursion altogether to avoid its unpredictable stack need. So in the spectrum of developers the band of coders who frequently deal with or encounter recursion is quite narrow IMO.
There are languages where recursion is the only construct available to deal with a collection of values so for those programmers recursive technique is a muscle memory. But I suspect there aren't as many professional users of such languages.
There are no problems that require recursion, since recursion and iteration are equivalent. You can do all of the things you list with a while loop plus state variables.
Recursion (especially in languages with pattern matching and other features common in the same set of languages in which recursion is more idiomatically favored) can provided solutions that are simultaneously more clear and more terse, but it doesn't expand the set of solvable problems.
I'm genuinely curious as to what kind of software you work on where you've never encountered a problem where a while loop required an ad hoc dynamic stack to track state, and so never had to consider the possibility of recursion as a viable, attractive alternative to that.
- You get to store only the state you strictly need and no more. A call stack stores all local variables and parameters for each frame, potentially wasting memory.
- You get to access previous elements. A call stack does not typically allow you to access data in previous stack frames.
- You get to push/pop multiple, variable number of elements per iteration. Call stacks typically only work with a fixed number of local variables/parameters per stack frame. The workarounds are to 1. dynamically allocate memory for each traversal, which causes lots of fragmentation, or 2. use an oversized fixed buffer for each stack frame, which causes memory waste, or 3. use an ‘alloca’-like construct, if your runtime supports it at all.
- You get to explicitly bound its memory usage and use custom handling/error reporting when your desired max depth is exceeded. Doing so with a call stack is typically a lot more clunky and bug-prone, as it requires exception handling and mutating global state.
- You can serialize/deserialize your stack to/from persistent storage however you like if you wish to pause traversal and resume it later. A call stack can only do this if it’s a ‘stackful coroutine’, which even if it is supported in your runtime of choice, gives you little to no control over where or how it is stored.
Some of the above problems have very neat recursive solution but the recursion is bad idea as it tends to various edge cases.
For example, "write a parser for a file format that contains arbitarily nested data strucutres", is exactly a problem where you DO NOT want to use recursion, and the reason is that "arbitarily" part. What if there is 1000 levels? What if there is 100 thousand levels?
DoS guys love these kinds of implementations. Just send very small, very well compressed payload and look how it all crashes and burns.
Have you ever seen a recursive call that also tries to detect/break cycles in the data structure?
I suspect the reason these things get left out the moment the developer decides on a recursive function is because it doesn't look nearly as much elegant then.
When you add this parameter to a recursive solution that is lacking it, your compiler will flag all the places where you are neglecting to pass it, and you just copy and paste d + 1 into those places. Or at worst, it will be caught as a run-ime insufficient parameters error.
At the end of this exercise, you not only have working code, but an inductive proof that d has the correct value everywhere. There are no cases where the depth is mismanaged, as could happen in some iterative solution where you must insert assignments to d, that do not create any sort of error when they are missing.
I would specify the data structure is ostensibly arbitrary, but conforming implementations can diagnose and reject more than 64 levels of nesting.
Anything needing more than 64 levels of nesting can safely be assumed to be DoS.
You would be correct if you wrote "Tail-optimizable recursion doesn't use more space than iteration in languages with tail call optimization"
I have used recursion, particularly due to my love for Erlang, just not in any software someone paid me to work on.
“Process all the files in this directory structure, counting them and summing their size.” Sure, you can solve that without recursing, but I’d trust a recursive algorithm to have fewer hidden bugs.
Lisps don't even pass the syntax phase.. newcomers won't have the tool-magic dopamine hook (say the first time you saw PHP map syntax, or ruby blocks or c# interop features). That's what people love at first, it gives a sense of a large new universe of powers.. while lisp is a naked white page of parens.. people just don't see what it's for. Other people will click on the deeper uniformity and lack of syntax as a lever.
Same goes for fp.. people see a soup of function, it's meaningless. Again the syntax hook is strong and instead of having f . g . h you get @f.then(g)[[h]] the brain gets tickled differently.
How C wasted the Pascal family languages in the 1980's.
The system has no external tooling (no description language). The application uses the C API to construct the needed types at initialization time.
I have a test case whereby a whole linked list is serialized and deserialized. The deserialize function mallocs the necessary nodes. There is a function to recursively free the object, using the serial type as a guide.
Another time, a little bit farther back, I worked with something recursive on the job fairly recently was speeding up the recursive file system tree walking function in BusyBox. It fails to take advantage of the Linux-specific d_type field in the inode. With that you can avoid stat calls.
This shaved a bunch of time off the boot of an embedded system, where BuxyBox's mdev program was scanning through device nodes in /sys.
Yet another time. I used recursion to shrink a Linux image by blowing away unused content in the initramfs. That time I used Python to write a kind of "garbage collection" program: it recursively traversed the contents of an initramfs filesystem, determining what files are reachable, marking those files and deleting the rest. Reachability was determined mainly by scanning the file contents for occurrences of the names of other files, with some other heuristics. The root node was the init script; the idea being that anything not required by the initramfs init script is bloat. The idea worked; we got a smaller initramfs that still booted fine.
The iterative solution is better in every regard. The state transitions are easier to see, and the sub procedures are easier to identify and split up, if necessary.
I have a degree in Computer Science, I did extremely well in that degree program, and I understand algorithms very well. I don't get the fascination with recursion in CS theory. It's just hiding state on the stack frame. That's why it's such a terrible tool, not just for engineering, but for teaching as well. Every recursive solution has an iterative solution, even if it's just tossing your state into a Stack data structure you manage yourself on the heap. You'd think you'd want to call that out for students.
Talking about loops vs recursion is interesting but it feels kinda like salt vs pepper to me. I like both and use both where appropriate.
While we are on the topic of similarly misused constructs, .stream() pipelines in Java are often clear and concise but as they get longer and hide more stuff in the stream, a loop would make far more sense and provide a much smaller runtime complexity.
Often, people will aspire to create “beautiful” code where they relate recursion/loops/streams/etc as something beautiful. In that vein, I don’t really care if code is “ugly” but I do care if it’s unmaintainable, needlessly slow or accidentally complex.
Mostly because I know my data structures are bounded, and the thing I'm writing isn't terribly performance sensitive.
The iterative solution is in the language we use a lot more verbose. I'd have to create a separate type to hold any parameters if there's more than one and I'd have to make my own stack of that type. So for most things I just go for plain old recursion.
It's chicken-and-egg. Programmers think of recursion as that thing that blows up the stack, so they don't demand that language implementers do a better job with it. Language implementers don't stop recursion from blowing up the stack because no-one is asking them to do so.
In order to get better at recursion, you must first understand recursion.
In order to get understand recursion, you must first understand recursion.
(yes, the lack of a base case makes the joke even worse:-)
Even better, dynamic programming is often more production worthy, since it can result in improved time complexity.
In Rust I'm spinning up another thread just to run recursive algorithms in, which I give a huge (1/2 size physical memory) stack size for running recursive algorithms in.
However, you still can't recover.
I was expecting Rust to give a compile-time error. Rust should refuse to compile a program that would overflow the stack. Unbounded recursion is such a case. Another case would be recursion that is bounded but very large. For example, if the recursion depth can be 128, then on Linux (with the 8 MiB stack default) it would not be allowable to have the stack frames be 64 kilobytes each. That should not compile.
Rust is not a memory-safe language.
carry on now...