Disclaimer, I don't know scheme and I'm about 1/5 into this book.
I think it's needed because the function you're replacing often has input parity (number of arguments taken) lower than the one you're replacing it with. You're kind of refactoring a function to put the output into a variable that you pass into it instead of returning it as output. Why do you need to replace each function with a logical equivalent? I'm not totally sure,
(Edit I read the context of your example more...)
if we take the example you gave, the standard version would raise an error if passed a non-list I think, which is different from returning failure. So if you pass something that's not a list, instead of getting () from your run, you raise an exception which is different.
There's another difference I realized, and a more important one. Your example comes from the definition of listo, and listo doesn't just check for whether something can be a list. it could be used to generate an infinite list if given an unbound variable. It does that because first the first element in the conde will succeed so you'll get (). After, pairo l will succeed by making the first element (reified_1) and the second element goes back to listo, this inner listo first returns () like the first result of the outer listo did, so you get (reified_1 . ()) and this can continue infinitely. This works because you have (cdro l d) with a fresh d, allowing d to be bound to a variable which is set by the recursive call to listo. You couldn't do that without creating the d variable by unnesting.
Explaining the parity point:
cdr(x) # get the rest elements of x
cdro(a x) # you "pass in" the equivalent to the output element of the first function into this logical version.
That means that if you have b = cdr(cdr(x))
You need to create a variable to hold the intermediate value like:
fresh(a)
cdro(a, x)
fresh(b)
cdro(b, a)
See the difference/reason? You're passing in your outputs. So you can't "nest" where everything sits inside the nested expression, you need to break it into lines to have space to assign your new variables.
Why does it have to be this way? I was considering whether you could avoid a lot of unnesting by overloading the functions based on parity. Something like I've written below (in python, assume caro is already defined). The point is that I think it's useless because the variable you bind to the first element of the list is new, not referred to anywhere else, so can't possibly be useful for applying any other constraints. The point of goals is to return sets of bindings that satisfy them, but those bindings must be on variables that are already defined or they're useless.
class UNDEF:
pass
def cdronew(a, b=UNDEF):
if b is UNDEF:
# assume that we need to fresh a variable and use that for our output
b = fresh('b')
# not a typo in order, we're assuming we wanted the first val of the list
# here is the problem, we'll bind b but noone outside this function knows about it so that binding is useless!
return cdro(b, a)
else:
return cdro(a, b)