Another tail recursion example

Terry Reedy tjreedy at udel.edu
Wed Jul 29 04:47:11 CEST 2015


On 7/28/2015 5:28 PM, Paul Rubin wrote:
> Chris Angelico was asking for examples of tail recursion that didn't
> have obvious looping equivalents.

Since there is a mechanical procedure for producing the equivalent 
*under the assumption that the function name will not be rebound*, he is 
effectively asking for a unicorn.

 > Here's an Euler problem solution using memoization and
 > (except that Python doesn't implement it)
 > tail recursion with an accumulator.

Python does tail recursion (and tail calls) just fine until stack space 
runs out.  What it does not do is automatic tail call or tail recursion 
conversion/elimination.  The example below illustrate

>      # Solution to Euler problem 14, using memoization
>      # https://projecteuler.net/problem=14
>
>      import sys
>      sys.setrecursionlimit(10000)
>
>      def memoize(func):
>          def mf(n, a, func=func, memo={}):
>              if n in memo: return memo[n]+a
>              return memo.setdefault(n,func(n,0))+a
>          return mf
>
>      @memoize
>      def col(n, a):
>          if n==1: return a
>          if n%2==0: return col(n//2, 1+a)
>          return col(3*n+1, 1+a)
 >
>      def main():
>          print max((col(i,0),i) for i in xrange(1,1000001))

(col(13,0) returns 9, but the chain has 10 items. Makes no difference 
for the problem as stated.)

Because Python allows names in python-coded modules to be rebound, 
including from other modules, and because names in python-coded function 
are resolved to objects only when the name is encountered at runtime, 
'recursiveness' is not an operational property of python-coded 
functions.  It is only an operational property of a particular call at a 
particular time.

If one wants to guarantee that a function operates 'recursively', in the 
sense of continuing at the top of the function with parameters rebound 
to altered arguments, then one should use iterative rather than 
recursive syntax.

> If I have it right, this should result in fewer calls to the col (short
> for Collatz) function than turning it into the obvious explicit loop.

So you should be glad that Python does *not* automatically replace the 
apparently-but-not-really-recursive tail call with an internal jump (ie, 
tail recursion elimination).

> To get the same gain you'd have to thread the memoization through the
> function in a probably ugly way.

Yes and no.

If one is computing a typical f(n) = g(f(n-1)) recursion, such as 
factorial, for 0, 1, 2, ..., the cache should be a list rather than a 
dict.  A smart cache will skip over values already computed.

Here is an intertwined version that you probably consider ugly.   It 
directly accesses a nonlocal cache from a closure instead of bouncing 
back and forth between function and memo wrapper.

def _fac():
     cache = [1, 1]
     csize = 2
     def _(n):
         nonlocal csize
         for i in range(csize, n+1):
             cache.append(cache[-1] * i)
             csize += 1
         return cache[n]
     return _
fac = _fac()

However, when one wants to generate and save an indefinite number of 
values for a function in f(0), f(1), ... order, then in Python one 
should use a generator.  It is easy to combine a specific generator with 
a generic cache.

class Gen_memo(list):
     def __init__(self, genf):
         self.next = genf().__next__
     def __call__(self, n):
         for i in range(self.__len__(), n+1):
             self.append(self.next())
         return self[n]

@Gen_memo
def fac2():
     n, fac = 0, 1
     while True:
         yield fac
         n += 1
         fac *= n

fac and fac2 pass this test:

for n in (1,2,6,3,5,7,4,5): print(n, fac(n), fac2(n))

> It's certainly doable, but as the
> saying goes, why trouble your beautiful mind over something like that.

I consider fac2 above to be more 'pythonic' in using a generator rather 
than a pseudo-tail-recursive function.  Some people like faster functions.

> The above solution seems pretty natural to me.

I agree for this function, which is somewhat unusual.


-- 
Terry Jan Reedy



More information about the Python-list mailing list