For me, the underutilized gold mine feels like logic languages (Prolog et al.) Though I know recursion is used there a lot, too.
For me, the underutilized gold mine feels like logic languages (Prolog et al.) Though I know recursion is used there a lot, too.
It's the same reason why I use let instead of a function when I want to encapsulate simple state across multiple statements. Sure they are both the same fundamentally, but one is much easier to reason about.
But, again, it's probably because I've been exposed to too much Scheme that this sits so prominently in my mind. I've never worked in a large scale Common Lisp code base.
Not really.
Lisp has imperative loops which process lists: prog/go, do, dolist, loop for in/on.
Then it has a bunch of higher order functions: mapcar, map, mapc, member, find, remove, delete, every, some, ...
For example this is the equivalent of an early 1960s procedure to map a function over a list for side-effects:
CL-USER 5 > (defun mapc-old (x f)
(prog ((m x))
loop
(when (null m) (return nil))
(funcall f (car m))
(pop m)
(go loop)))
MAPC-OLD
As you can see above, the above uses a PROG providing a local variable M, a tag LOOP, and is using a GO statement to pass control to the position of the LOOP tag.We can then process a list. Here we print the squares of the elements.
CL-USER 6 > (mapc-old '(1 2 3 4)
(lambda (e)
(print (expt e 2))))
1
4
9
16
NIL
Code using imperative loops to implement higher-order functions were common then. Then with macros one is hiding an imperative loop implementation behind a macro transformer: here the DOLIST macro CL-USER 12 > (dolist (e '(1 2 3 4))
(print (expt e 2)))
1
4
9
16
NIL
Then one invented complex iteration sub-languages, here the LOOP macro in a tiny example. CL-USER 17 > (loop for e in '(1 2 3 4)
do (print (expt e 2)))
1
4
9
16
NIL
Any list processing function, which is threatening to create stack overflows (even though some implementations can increase a stack at an stackoverflow error), is best rewritten into a function which does not cause stack overflows. Lisp uses traditionally imperative loop constructs for that. Scheme often uses tail-recursive functions for that.In practice, you want to recurse only on the elements of a list. And that's just how you'd handle a tree whose nodes are vectors instead of lists. So the implication that lists lead to recursion is a red herring.
Scheme is misleading because of its fetish of implementing iteration with tail calls.
https://stackoverflow.com/questions/49912204/why-there-is-no...
The SICP book had a paragraph on the distinction before recursive process and recursive procedure. I can't remember which was which, but the point there was that recursive procedure could be an iterative process if it's written with tail recursion in mind, in which case it would not overflow the stack.
I've never worked in the source tree of a large Common Lisp program. Though I've bought the books and read the tutorials etc.
Recursion appeals to me because of immutability:
I do not want to do a summation by writing the wrong answer to memory, and then updating it in-place until it's the right answer. It makes me have to think not only about what the right answer is, but when it is.
But can't I just use stack variables? Well, I don't think summation of primitive ints using stack variables is too taxing, but what about computing a word-count instead of a summation? Then your 'stack variables' are pointers into Strings and mutable HashMaps. When you return the result, do you make a defensive copy? How deeply do you copy? Your clone of a HashMap can still reference mutable data in your original HashMap.
Do you name your variable 'result' when it does not yet contain the result?
You might have to think about the primitive/object split - when is it pass-by-value and when is it pass-by-pointer? Do you have in-values and out-values? With immutability, you only have values, and you only pass-by-value.
If you're sharing objects across threads, and you decide to lock - is that sufficient? What if the caller takes the lock, does a get(), releases the lock, and then starts mutating the gotten value, bypassing the protection of the lock. Now you're thinking about defensive copies and maybe deadlocks too. Just share an immutable value without a lock.
Sometimes recursion is aesthetically better, but usually it's just what you're left with after you take a big hammer to mutability and its surrounding issues.
And most of the time (in Haskell but I'm guessing in Lisps too) you don't write out direct recursions, but instead use maps and folds which have recursion under the hood.
A function should be a "black box". I don't care what happens inside it as long its externally observable behavior is pure.
On "classical" computing hardware, it's fine. On present-day architectures we're better off writing with "wider" operations across vectors rather than depth-first-ish recursive operations on scalars.
But yes, for some domains, the performance doesn't matter.
I just also find recursion more difficult to read in some contexts. In others, things like tree transformation etc. it's easier to read, for sure.
But I am also a fan of immutable, or at least copy-on-write, data, expressed in series of functional applications, etc. etc.
The Reasoned Schemer is an accessible introduction to that space if you’re not put off by the socratic-themed dialogue.
& the Barliman demo is still pretty exciting even after LLM codegen.
If you want to write one yourself, it's pretty easy: https://www.youtube.com/watch?v=y1bVJOAfhKY
Scheme likes to tout it because of tail call optimization. Common Lisp can do it, but doesn't really shove it as a first principle of "Common Lisping" (and CL does not dictate tail call optimization).
I don't use it (save when it's necessary). I just iterate using the supplied iterators (do, dolist, loop, whatever).
I can't speak to other functional languages.
Recursion is sometimes the easiest way to reason about a problem though, so it's nice to have decent support and TCO.
Yes, it's one of the things that allow FP algorithms to be more declarative. It's difficult to seriously argue that recursion is fundamentally more complex-- the contrary argument is more obvious to me.
Everything that's left is the fact that it requires people to unlearn things, which is uncomfortable and requires effort, but unfamiliarity is not an indicator of complexity.