New tail recursion decorator

Collapse
This topic is closed.
X
X
 
  • Time
  • Show
Clear All
new posts
  • Kay Schluehr

    New tail recursion decorator



  • Diez B. Roggisch

    #2
    Re: New tail recursion decorator

    Kay Schluehr wrote:
    [color=blue]
    > http://aspn.activestate.com/ASPN/Coo.../Recipe/496691[/color]

    Neat.

    Diez

    Comment

    • Kay Schluehr

      #3
      Re: New tail recursion decorator


      Diez B. Roggisch wrote:[color=blue]
      > Kay Schluehr wrote:
      >[color=green]
      > > http://aspn.activestate.com/ASPN/Coo.../Recipe/496691[/color]
      >
      > Neat.
      >
      > Diez[/color]

      Hi Diez,

      for all those who already copied and pasted the original solution and
      played with it I apologize for radical changes in the latest version (
      the recipe is on version 1.5 now ! ). The latest implementation is
      again a lot faster than the previous one. It does not only get rid of
      exceptions but also of stack-frame inspection.

      Regards,
      Kay

      Comment

      • Duncan Booth

        #4
        Re: New tail recursion decorator

        Kay Schluehr wrote:
        [color=blue]
        >
        > Diez B. Roggisch wrote:[color=green]
        >> Kay Schluehr wrote:
        >>[color=darkred]
        >> > http://aspn.activestate.com/ASPN/Coo.../Recipe/496691[/color]
        >>
        >> Neat.
        >>
        >> Diez[/color]
        >
        > Hi Diez,
        >
        > for all those who already copied and pasted the original solution and
        > played with it I apologize for radical changes in the latest version (
        > the recipe is on version 1.5 now ! ). The latest implementation is
        > again a lot faster than the previous one. It does not only get rid of
        > exceptions but also of stack-frame inspection.
        >
        > Regards,
        > Kay
        >[/color]
        I'm not convinced by this. You have to recognise that the function is using
        tail recursion, and then you have to modify the code to know that it is
        using tail recursion. This is not always trivial. For example, the given
        example is:

        @tail_recursion
        def factorial(n, acc=1):
        "calculate a factorial"
        if n == 0:
        return acc
        res = factorial(n-1, n*acc)
        return res

        but a more common way to write the function would be:

        @tail_recursion
        def factorial(n):
        "calculate a factorial"
        if n == 0:
        return 1
        return n * factorial(n-1)

        which won't work because it isn't actually tail recursion, but it looks
        sufficiently close to tail recursion that it would probably mislead a lot
        of people into expecting it will work. If you are going to have to rewrite
        functions in a stilted manner, and they use simple tail recursion, then why
        not just factor out the tail recursion in the first place.

        My other problem with this is that the decorator is very fragile although
        this may be fixable. e.g. (using the published example) an exception
        thrown inside the function makes future calls return garbage:
        [color=blue][color=green][color=darkred]
        >>> factorial(3)[/color][/color][/color]
        6[color=blue][color=green][color=darkred]
        >>> factorial('a')[/color][/color][/color]

        Traceback (most recent call last):
        File "<pyshell#5 >", line 1, in -toplevel-
        factorial('a')
        File "<pyshell#1 >", line 12, in result
        tc = g(*args,**kwd)
        File "<pyshell#3 >", line 6, in factorial
        res = factorial(n-1, n*acc)
        TypeError: unsupported operand type(s) for -: 'str' and 'int'[color=blue][color=green][color=darkred]
        >>> factorial(3)[/color][/color][/color]
        ('continue', (3,), {})

        Comment

        • Michele Simionato

          #5
          Re: New tail recursion decorator

          Kay Schluehr wrote:[color=blue]
          > for all those who already copied and pasted the original solution and
          > played with it I apologize for radical changes in the latest version (
          > the recipe is on version 1.5 now ! ). The latest implementation is
          > again a lot faster than the previous one. It does not only get rid of
          > exceptions but also of stack-frame inspection.[/color]

          This is spectacular!!

          I would rewrite it as follows:

          CONTINUE = object() # sentinel value returned by iterfunc

          def tail_recursive( func):
          """
          tail_recursive decorator based on Kay Schluehr's recipe

          """
          var = dict(in_loop=Fa lse, cont=True, argkw='will be set later')
          # the dictionary is needed since Python closures are read-only

          def iterfunc(*args, **kwd):
          var["cont"] = not var["cont"]
          if not var["in_loop"]: # start looping
          var["in_loop"] = True
          while True:
          res = func(*args,**kw d)
          if res is CONTINUE:
          args, kwd = var["argkw"]
          else:
          var["in_loop"] = False
          return res
          else:
          if var["cont"]:
          var["argkw"] = args, kwd
          return CONTINUE
          else:
          return func(*args,**kw d)
          return iterfunc

          Using my decorator module 'tail_recursive ' can even be turned in a
          signature-preserving
          decorator. I think I will add this great example to the documentation
          of the next version
          of decorator.py!

          Michele Simionato

          Comment

          • Michele Simionato

            #6
            Re: New tail recursion decorator

            Michele Simionato wrote:[color=blue]
            > Using my decorator module 'tail_recursive ' can even be turned in a
            > signature-preserving
            > decorator. I think I will add this great example to the documentation
            > of the next version
            > of decorator.py!
            >
            > Michele Simionato[/color]

            Done: see
            http://www.phyast.pitt.edu/~micheles.../decorator.zip and


            Comment

            • Felipe Almeida Lessa

              #7
              Re: New tail recursion decorator

              Em Ter, 2006-05-09 às 23:30 -0700, Kay Schluehr escreveu:[color=blue]
              > http://aspn.activestate.com/ASPN/Coo.../Recipe/496691[/color]

              Is it thread safe?

              --
              Felipe.

              Comment

              • Carl Banks

                #8
                Re: New tail recursion decorator


                Michele Simionato wrote:[color=blue]
                > CONTINUE = object() # sentinel value returned by iterfunc
                >
                > def tail_recursive( func):
                > """
                > tail_recursive decorator based on Kay Schluehr's recipe
                > http://aspn.activestate.com/ASPN/Coo.../Recipe/496691
                > """
                > var = dict(in_loop=Fa lse, cont=True, argkw='will be set later')
                > # the dictionary is needed since Python closures are read-only
                >
                > def iterfunc(*args, **kwd):
                > var["cont"] = not var["cont"]
                > if not var["in_loop"]: # start looping
                > var["in_loop"] = True
                > while True:
                > res = func(*args,**kw d)
                > if res is CONTINUE:
                > args, kwd = var["argkw"]
                > else:
                > var["in_loop"] = False
                > return res
                > else:
                > if var["cont"]:
                > var["argkw"] = args, kwd
                > return CONTINUE
                > else:
                > return func(*args,**kw d)
                > return iterfunc[/color]

                CONTINUE could be put inside tail_recursive, couldn't it? And to
                squeeze a little more speed out of it, var could be a list (saves a
                hash lookup).

                Cool decorator.


                Carl Banks

                Comment

                • Tim N. van der Leeuw

                  #9
                  Re: New tail recursion decorator

                  [...][color=blue][color=green]
                  > >[/color]
                  > I'm not convinced by this. You have to recognise that the function is using
                  > tail recursion, and then you have to modify the code to know that it is
                  > using tail recursion. This is not always trivial. For example, the given
                  > example is:
                  >
                  > @tail_recursion
                  > def factorial(n, acc=1):
                  > "calculate a factorial"
                  > if n == 0:
                  > return acc
                  > res = factorial(n-1, n*acc)
                  > return res
                  >
                  > but a more common way to write the function would be:
                  >
                  > @tail_recursion
                  > def factorial(n):
                  > "calculate a factorial"
                  > if n == 0:
                  > return 1
                  > return n * factorial(n-1)
                  >
                  > which won't work because it isn't actually tail recursion, but it looks
                  > sufficiently close to tail recursion that it would probably mislead a lot
                  > of people into expecting it will work. If you are going to have to rewrite
                  > functions in a stilted manner, and they use simple tail recursion, then why
                  > not just factor out the tail recursion in the first place.
                  >[/color]
                  [...]

                  Hi Duncan,

                  I don't know why it wouldn't work this way, or why it isn't
                  tail-recursion?

                  I tried the tail_recursion decorator from the cookbook-recipe with both
                  definitions of factorial, and I tried both definitions of the factorial
                  function with and without tail_recursion decorator.

                  In all four cases I get the same results, so it does work with both
                  definitions of factorial(), even if (according to you) the second
                  definition is not proper tail-recursion.

                  Using the tail-recursion decorator (the version that does not inspect
                  the stackframes) I get a small performance-increase over using the
                  factorial-function undecorated.
                  However, calculating factorial(1000) with the factorial-function as
                  defined in the cookbook-recipe is much much faster than calculating the
                  same factorial(1000) with the factorial-function you gave!
                  I cannot yet explain why the first function has so much better
                  performance than the second function - about a factor 10 difference,
                  in both python2.4.3 and python 2.5a2


                  Cheers,

                  --Tim

                  Comment

                  • Michele Simionato

                    #10
                    Re: New tail recursion decorator

                    Tim N. van der Leeuw wrote:[color=blue]
                    >
                    > I don't know why it wouldn't work this way, or why it isn't
                    > tail-recursion?[/color]
                    [color=blue]
                    >From the google page do "define: tail recursion"[/color]
                    [color=blue]
                    > I tried the tail_recursion decorator from the cookbook-recipe with both
                    > definitions of factorial, and I tried both definitions of the factorial
                    > function with and without tail_recursion decorator.
                    > In all four cases I get the same results, so it does work with both
                    > definitions of factorial(), even if (according to you) the second
                    > definition is not proper tail-recursion.[/color]

                    For me factorial(1001) with the second definition does not work, I get
                    the recursion limit (which is what I expect). I suppose the recursion
                    limit is higher on your system, but definitely you should reach it at
                    some point with the non tail-recursive version of factorial.
                    [color=blue]
                    > Using the tail-recursion decorator (the version that does not inspect
                    > the stackframes) I get a small performance-increase over using the
                    > factorial-function undecorated.
                    > However, calculating factorial(1000) with the factorial-function as
                    > defined in the cookbook-recipe is much much faster than calculating the
                    > same factorial(1000) with the factorial-function you gave!
                    > I cannot yet explain why the first function has so much better
                    > performance than the second function - about a factor 10 difference,
                    > in both python2.4.3 and python 2.5a2[/color]

                    It is because the decorator is doing is job (converting a long
                    recursion in a loop)
                    only with the first function, which is properly tail recursive. Just as
                    Duncan said.

                    Michele Simionato

                    Comment

                    • Tim N. van der Leeuw

                      #11
                      Re: New tail recursion decorator

                      Hi Michele,

                      I'm sorry, but you misunderstood me.

                      There are two definitions of the factorial() function, one given by the
                      OP and the other given by Duncan.

                      I tested both factorial() definitions with, and without the
                      tail_recursion decorator (the version of the OP). So I had 4
                      factorial-functions defined in my test-file:

                      @tail_recursion
                      def factorial(n, acc=1):
                      # do the stuff
                      pass

                      def factorial_r(n, acc=1):
                      # do the stuff
                      pass

                      @tail_recursion
                      def factorial2(n):
                      # do the stuff
                      pass

                      def factorial2_r(n) :
                      # do the stuff
                      pass

                      All four functions give the same output for the tests I did (n=120, and
                      n=1000).
                      Using timeit, both factorial(1000) and factorial2(1000 ) are somewhat
                      faster than factorial_r(100 0) respectively factorial2_r(10 00).
                      However, factorial(1000) and factorial_r(100 0) are both 10x faster than
                      factorial2(1000 ) and factorial2_r(10 00).

                      It's the latter performance difference which I do not understand.

                      The other thing I do not understand, due to my limited understanding of
                      what is tail-recursion: factorial2 (Duncan's definition) is not proper
                      tail-recursion. Why not? How does it differ from 'real' tail recursion?
                      And if it's not proper tail-recursion and therefore should not work,
                      then how comes that the tests I do show it to work? And I seemed to
                      consistently get a slightly better performance from factorial2(1000 )
                      than from factorial2_r(10 00).


                      NB: Regarding the recursion limits, I don't know what would be the
                      stacklimit on my system (Python 2.4.3 on WinXP SP2). I already
                      calculated the factorial of 500000 using the recursive (non-decorated)
                      function...


                      Cheers,

                      --Tim

                      Comment

                      • Duncan Booth

                        #12
                        Re: New tail recursion decorator

                        Tim N. van der Leeuw wrote:
                        [color=blue]
                        > The other thing I do not understand, due to my limited understanding of
                        > what is tail-recursion: factorial2 (Duncan's definition) is not proper
                        > tail-recursion. Why not? How does it differ from 'real' tail recursion?[/color]

                        Tail recursion is when a function calls itself and then immediately returns
                        the result of that call as its own result. So long as nothing except
                        returning the result needs to be done it is possibly to avoid the recursive
                        call altogether.

                        This function is tail recursive:

                        @tail_recursion
                        def factorial(n, acc=1):
                        "calculate a factorial"
                        if n == 0:
                        return acc
                        res = factorial(n-1, n*acc)
                        return res

                        but this one isn't:

                        @tail_recursion
                        def factorial2(n):
                        "calculate a factorial"
                        if n == 0:
                        return 1
                        return n * factorial2(n-1)

                        because when the inner call to factorial2() returns the function still has
                        to do some work (the multiplication) .

                        I don't understand your comments about speed differences. If you try to run
                        factorial2() as defined above then it simply throws an exception: there
                        are no results to compare. My guess is that when you wrote:

                        @tail_recursion
                        def factorial2(n):
                        # do the stuff
                        pass

                        your 'do the stuff' actually had an erroneous call to 'factorial'. If you
                        are going to rename the function you have to rename the recursive calls as
                        well. (At least, that's what I forgot to do when I first tried it and
                        couldn't understand why it gave me an answer instead of crashing.)

                        The decorator also fails for functions which are tail-recursive but which
                        contain other non-tail recursive calls within themselves. For example I
                        would be pretty sure you couldn't write a working implementation of
                        Ackermann's function using the decorator:

                        def Ack(M, N):
                        if (not M):
                        return( N + 1 )
                        if (not N):
                        return( Ack(M-1, 1) )
                        return( Ack(M-1, Ack(M, N-1)) )

                        Comment

                        • Tim N. van der Leeuw

                          #13
                          Re: New tail recursion decorator


                          Duncan Booth wrote:[color=blue]
                          > Tim N. van der Leeuw wrote:
                          >[/color]
                          [...][color=blue]
                          > @tail_recursion
                          > def factorial2(n):
                          > # do the stuff
                          > pass
                          >
                          > your 'do the stuff' actually had an erroneous call to 'factorial'. If you
                          > are going to rename the function you have to rename the recursive calls as
                          > well. (At least, that's what I forgot to do when I first tried it and
                          > couldn't understand why it gave me an answer instead of crashing.)[/color]
                          [...]

                          Duncan,

                          You're totally right. Somehow, I had managed to completely overlook
                          this. Oops!
                          My apologies! :)

                          --Tim

                          Comment

                          • Kay Schluehr

                            #14
                            Re: New tail recursion decorator

                            Duncan Booth wrote:
                            [color=blue]
                            > The decorator also fails for functions which are tail-recursive but which
                            > contain other non-tail recursive calls within themselves. For example I
                            > would be pretty sure you couldn't write a working implementation of
                            > Ackermann's function using the decorator:
                            >
                            > def Ack(M, N):
                            > if (not M):
                            > return( N + 1 )
                            > if (not N):
                            > return( Ack(M-1, 1) )
                            > return( Ack(M-1, Ack(M, N-1)) )[/color]

                            Definitely. The translation into a proper tail-recursive form is
                            non-trivial but nevertheless possible as demonstrated by the following
                            Ackermann implementation:

                            @tail_recursion
                            def ack(m,n,s=[0]): # use a stack-variable s as "accumulato r"
                            if m==0:
                            if s[0] == 1:
                            return ack(s[1]-1,n+1,s[2])
                            elif s[0] == 0:
                            return n+1
                            elif n==0:
                            return ack(m-1,1,s)
                            else:
                            return ack(m,n-1,[1,m,s])

                            Regards,
                            Kay

                            Comment

                            • Alexander Schmolck

                              #15
                              Re: New tail recursion decorator

                              Duncan Booth <duncan.booth@i nvalid.invalid> writes:
                              [color=blue]
                              > Tim N. van der Leeuw wrote:
                              >[color=green]
                              > > The other thing I do not understand, due to my limited understanding of
                              > > what is tail-recursion: factorial2 (Duncan's definition) is not proper
                              > > tail-recursion. Why not? How does it differ from 'real' tail recursion?[/color]
                              >
                              > Tail recursion is when a function calls itself and then immediately returns
                              > the result of that call as its own result.[/color]

                              I think the definition is broader than that so that these two functions would
                              also be tail-recursive (i.e. the tail call doesn't have to be a self-tail
                              call; I might be mistaken, don't have a good reference at hand; however
                              "properly tail recursive" certainly refers to being able to do the below
                              without exhausting the stack even for large n, not just transforming self-tail
                              calls to a loop, which is sort of limited usefulness anyway):

                              def even(n):
                              return n == 0 or not odd(n-1)

                              def odd(n):
                              return n == 1 or not even(n-1)

                              'as

                              Comment

                              Working...