Tail-call elimination is nice when the tail is known statically, but it is, as you said, just an optimization at that point.
It becomes much more interesting (and important!) when you don't statically know what the tail-call will be.
For example, consider embedding a Prolog-like logic language into Haskell. The easiest way I've seen to implement backtracking is by passing a "success" and "failure" continuation around. (see http://okmij.org/ftp/papers/LogicT.pdf)
-- success continuation
type SK ans a = a -> FK ans -> ans
-- failure continuation
type FK ans = ans
-- LogicT is a monad transformer, where m is the type of the
-- monad to be transformed. For example, LogicT IO a could
-- include side effects as part of the computation of a
-- SFKT = "success/failure continuation thread"
newtype LogicT m a = SFKT (forall ans. SK (m ans) a -> FK (m ans) -> m ans)
See the paper for the rest of the details on how this is a Monad and how to use it to implement MonadPlus and a set of other interesting operations like "fair disjunction".
However, these continuation-passing styles rely heavily on the fact that tail-calling a continuation function runs in constant stack space.
Continuation Passing Style
It becomes much more interesting (and important!) when you don't statically know what the tail-call will be.
For example, consider embedding a Prolog-like logic language into Haskell. The easiest way I've seen to implement backtracking is by passing a "success" and "failure" continuation around. (see http://okmij.org/ftp/papers/LogicT.pdf)
See the paper for the rest of the details on how this is a Monad and how to use it to implement MonadPlus and a set of other interesting operations like "fair disjunction".
However, these continuation-passing styles rely heavily on the fact that tail-calling a continuation function runs in constant stack space.