I am using Racket ISL+ to write a recursion that is counting a series of structures. If the structures fails some parameters, I want to return a value of #false. However, at one point during recursion, I know the computer is getting 1 + 1 + 1 + 1 + 1 + 1 + #false, which is giving me an error.
Is there a way for me to state that if an error is found to just return a value of #false from the original call?
When a value is returned from some procedure deep in a recursive process, the procedure that receives that value must be able to handle it. If a recursive procedure can return either of two distinct types, e.g., a boolean or a number, then that procedure will need to test the returned value before processing it with a procedure that requires a single type.
Using A Continuation
It is possible to get a continuation and then to use that to escape a recursive process. This is probably not the solution OP is looking for; I don't believe that the HTDP Student Languages provide a means of getting continuations. In plain Racket you could use
call/ccto grab a continuation:This is not the place to develop the details of continuations, but briefly,
call/ccarranges it so thatbreakin the above code is an escape procedure that, when called, returns its argument (#fhere) to the continuation of the computation with respect to the call tocall/cc. Here, when the first element of the input is anumber?, 1 is added to the result of counting the rest of the input; but, when the first element of the input is not anumber?(and the input is not an empty list) thebreakprocedure is called. Whenbreakis invoked in the recursive process described byloopcontrol jumps to that continuation, which is after the call tocall/cc, thereby escaping the recursive process; the value#fis given to the continuation, so that#fis then returned bycount-numbers-or-f.Doing Without Continuations
Continuations are a classic way to achieve this sort of a non-local exit from a loop, but you can get a similar result, with some careful design, using less exotic means:
Here, if the car of
xsis not a number (andxsis not an empty list), then#fis returned to the previous caller. Otherwiserrepresents the result of callingcount-numbers-of-f-1on the cdr ofxs. Ifris not a number (because the subsequent caller encountered a non-numeric element and returned#f), then#fis returned. Otherwise the additive process grows.The upshot of this is that if an element that is not a number is encountered,
#fis immediately passed back through all previous stack frames to the originating call, otherwise the summation is carried out. It is easy to make a mistake designing procedures like this so that they appear to work, but end up doing a lot of unnecessary work by traversing all of the input (or worse). See the end of the answer for a discussion of what could go wrong here.Comparison
Either of the above definitions will work:
Here are a couple of traces of the second version:
You can see that when a failing element is encountered, the recursion is immediately ended, but control still has to pass back through the previous stack frames. With the version using
call/ccthe control simply escapes from the recursive process, without passing through any previous frames.Here is a version of the first procedure using
call/ccwhich is modified so that the recursive process can be traced:Here is a trace showing that when
#fis returned, it is returned directly without passing through the previous stack frames. Theloopprocedure does not return at all when#fis encountered; insteadloopcalls thebreakprocedure, passing#fto it. Thebreakprocedure jumps to the continuation of the computation, returning#fto that point. Since the continuation picks up the computation at the end of thecount-numbers-or-fprocedure,#fis just returned fromcount-numbers-or-f.What Could Go Wrong?
A well-designed recursive procedure can do pretty well here, but it is easy to implement a poorly-designed solution that can have serious performance problems.
This section is about showing how things can go off the rails with a poorly thought-out design. All of the procedures below work in that they return correct results, but they have different performance characteristics, and the worst of them is practically unusable. The takeaway should be that it is important to, at a minimum, test new procedures over a range of anticipated inputs to check for surprises. Possibly a second takeaway should be that multiple recursive calls in a procedure definition should be thought about carefully. A great tool to assist in testing is
trace; in Racket you can(require racket/trace)and then(trace my-procedure)to instrumentmy-procedureso that any calls tomy-procedureare annotated with a procedure trace. This is how the traces in this answer have been generated.We have already seen a trace of
count-numbers-or-f-1above, showing that when a failing element is encountered#fis passed back to the previous callers until the original call is reached.Consider this poorly implemented version:
Here, since
count-numbers-or-f-2is called before checking whether the result of the remainder of the computation is anumber?, the recursive process continues until reaching the base case(null? xs). This does not help at all when failing input is encountered:Now consider this truly terrible implementation that attempts to solve the last problem. Here the result of the rest of the computation is checked before adding it to the final result. This works when the input contains a failing element; but in cases where the entire input is counted
count-numbers-or-f-3is called twice, leading to exponential time complexity.With failing input things look fine:
But with input that should be counted, things go horribly wrong. So wrong, in fact, that I had to use smaller input to illustrate the process in a reasonable amount of space:
Here is a procedure that can be used to time the above procedures:
I have used a list of 800,001 numbers for testing, with an
'xplaced in the middle of the list. Thecount-numbers-or-fusing continuations performs the best:Compared with
count-numbers-or-f-1which ends the recursion when failing input is encountered, the first version is a bit faster due to the immediate escape using a continuation:The bad version which traverses all of the input even when failing input is encountered does worse yet:
It looks like the better non-escaping implementation,
count-numbers-or-f-1, is about 5 times slower thancount-numbers-or-f(using continuations), and the poorly implementedcount-numbers-of-f-2is about 28 times slower thancount-numbers-or-f. But at least all of these implementations are roughly O(n) in time complexity. The worst version,count-numbers-or-f-3is O(2ⁿ). There isn't enough time in the universe to wait for this procedure to operate on a list of 800,001 elements. We will have to time using much smaller inputs:Here you can see that increasing the size of the input list by 1 element roughly doubles the time to complete the computation in the worst cases, and those worst cases are precisely those cases in which you want the computation to complete, i.e., when you want to get a count of how many numbers are in a list.