0

While developing a classical exercise piece of code for odd and even functions in MIT/GNU Scheme (rel 9.2), I encountered a problem that my code does not terminate for a big integer value. First, I tested the following code, which processes both positive and negative values:

(define error-message-number "Error. x must be a number")

(define odd?
  (lambda (x)
    (cond 
      ((not (integer? x)) error-message-number)
      ((= x 0) #f)
      ((< x 0) (even? (+ x 1))) ;for negatives
      (else (even? (- x 1)))))) ;if number n is odd then n - 1 is even 

(define even?
  (lambda (x)
    (cond 
      ((not (integer? x)) error-message-number)
      ((= x 0) #t)
      ((< x 0) (odd? (+ x 1))) ;for negatives
      (else (odd? (- x 1)))))) ;if number n is even then n - 1 is odd

The calls (assert (equal? #f (even? 100000000001))) and (assert (equal? #t (odd? -100000000001))) do not terminate on my machine, while, e.g. (assert (equal? #f (even? 1001))) and (assert (equal? #t (odd? -1001))) do. My first thought was that the code is not optimized for proper tail recursion, while I have not one, but two tail calls in each function. So, I decided to simplify the task and make a take for positive integers only, like this:

(define error-message-positive "Error. x must be a nonnegative number")

(define odd-positive?
  (lambda (x)
    (cond 
      ((not (integer? x)) error-message-number)
      ((= x 0) #f)
      ((< x 0) error-message-positive) ;for negatives
      (else (even? (- x 1)))))) ;if number n is odd then n - 1 is even 

(define even-positive?
  (lambda (x)
    (cond 
      ((not (integer? x)) error-message-number)
      ((= x 0) #t)
      ((< x 0) error-message-positive) ;for negatives
      (else (odd? (- x 1)))))) ;if number n is even then n - 1 is odd

But this version does not return either for big integers. So, I have these related questions:

  1. Features. Are mutually recursive functions in MIT/GNU Scheme optimized at all?
  2. Diagnostics. Is there any way one can tell the functions were indeed optimized for mutual tail recursion by Scheme compiler/interpreter. So, how can one tell the problem is in (absence of) tail recursion optimization, or in some other thing.
  3. What is proper mutual tail recursion? Does my initial code qualify for optimization? Does my second take qualify for it?
Will Ness
  • 70,110
  • 9
  • 98
  • 181
Pavlo Maistrenko
  • 187
  • 2
  • 12

1 Answers1

7
  1. What is proper mutual tail recursion?

Just like your code, both versions of them.

  1. Diagnostics.

Empirical orders of growth FTW!

Your diagnosis just might be incorrect. In particular, on my machine, in Racket, the expected time of your code to finish is 40 minutes. And it does seem to run in constant memory overall.

Yes, even while running in constant space it still takes time linear in the magnitude of argument. How do I know? I simply measured it in clock wall time, and it indeed scales as linear i.e. n1 power law. Approximately. (i.e. whether it measured as 1.02 or 0.97, it still indicates linear growth rate. approximately. which is all that matters).

See also:

  1. Features. Are mutually recursive functions in MIT/GNU Scheme optimized at all?

It must be so, since tail call optimization is in the language specification. And TCO is more than just tail recursion, as I understand it even if the decision what to call next is dynamic (let alone static, evident in code as it is in your case) it still must run in constant stack space when one tail call eventually leads back to entering the same function again. Tail call is tail call, whatever is called, and it must be optimized. I don't have an official quotation ready at the moment.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
  • 1
    Will, thank you very much for the answers and the links. I understand it is a slow algorithm in terms of time O(n). I will study the links and try to make some optimizations, if any possible, to have the runtime reduced. P.S. I've used a pretty old slow laptop, so this may be the reason the code "never terminated." – Pavlo Maistrenko Sep 15 '20 at 16:49
  • you're welcome. :) or maybe you haven't tried waiting for 40 minutes? :) btw you can run it for smaller sizes which take only a few seconds, then run for double the size, then calculate the projected time. about making it faster: you're essentially counting in ones towards zero. you can try counting by twos. or by fours. or by eights. or by 16s. see? you can gradually increase the subtraction amount *by doubling it*, checking that you're still above 0. thus total time will be logarithmic. of course we can always divide by 2 and check the remainder, but whats the fun in that? :) – Will Ness Sep 15 '20 at 19:03
  • 2
    [What should I do when someone answers my question?](https://stackoverflow.com/help/someone-answers) – ad absurdum Sep 16 '20 at 02:22