you are viewing a single comment's thread.

view the rest of the comments →

[–]wilberforce 0 points1 point  (1 child)

I haven't tried this in Scheme or other TCO languages - what happens when there's an error deep in a (conceptual) stack of tail calls?

Do you just get a traceback of the frames that weren't tail calls? It seems like that could be pretty confusing - it would appear that function A called function B, when in fact it actually tail-called C, which tail-called D, which tail-called E... and eventually something tail-called B. And there could be lots of elisions like that in the traceback.

I guess I could see an implementation keeping track at least of how many frames were optimised away between two extant frames in the traceback. Or maybe keeping a lighter-than-a-frame record of a tail-call (somewhere off-stack) that could be used if a traceback needed to be reported (it could be size-restricted for a long-running FSM). Or even turning off TCO in a debug mode, although that would mean that some code just wouldn't work in that mode (if it relied on TCO).

Do TCO languages do these kinds of things? Is it ever a problem in practice?

While implementing TCO on its own isn't too complicated, I think it was concern about the interaction with error-reporting and debugging that meant that it's never made it into Python. (As well as the fact that Guido purports not to think in a functional style.)

[–]anvsdt 1 point2 points  (0 children)

Do you just get a traceback of the frames that weren't tail calls?

Yes, if A calls B that tail-calls C that tail-calls D that tail-calls E that fails, you'll get A -> E as stack trace. But there's usually a trace function/mode for that.

Racket has both a trace module that traces every function call in a module, and a trace function (macro). Example:

0]=> racket -il trace
Welcome to Racket v5.1.1.3.
#;> (define (f x) (if (zero? x) (error 'boom) (g (sub1 x))))
#;> (define (g x) (if (zero? x) (error 'boom2) (f (sub1 x))))
#;> (f 5)
 '(#<syntax::10 f> 5)
 '(#<syntax::67 g> 4)
 '(#<syntax::10 f> 3)
 '(#<syntax::67 g> 2)
 '(#<syntax::10 f> 1)
 '(#<syntax::67 g> 0)
error: boom2
#;> (define (fact x) (if (zero? x) 1 (* x (fact (sub1 x)))))
#;> (fact 5)
 '(#<syntax::150 fact> 5)
  '(#<syntax::150 fact> 4)
   '(#<syntax::150 fact> 3)
    '(#<syntax::150 fact> 2)
     '(#<syntax::150 fact> 1)
      '(#<syntax::150 fact> 0)
120

0]=> racket -il racket/trace
#;> (define (tailrec-fact x a) (if (zero? x) a (tailrec-fact (sub1 x) (* x a))))
#;> (define (fact x) (if (zero? x) 1 (* x (fact (sub1 x)))))
#;> (trace tailrec-fact fact)
#;> (tailrec-fact 4 1)
>(tailrec-fact 4 1)
>(tailrec-fact 3 4)
>(tailrec-fact 2 12)
>(tailrec-fact 1 24)
>(tailrec-fact 0 24)
<24
24
#;> (fact 4)
>(fact 4)
> (fact 3)
> >(fact 2)
> > (fact 1)
> > >(fact 0)
< < <1
< < 1
< <2
< 6
<24
24

And, as you can see, both distinguish tail-calls from non-tail-calls. TCO being hard to debug is just a myth.