classification
Title: More functools functions
Type: enhancement Stage: resolved
Components: Library (Lib) Versions: Python 3.3
process
Status: closed Resolution: rejected
Dependencies: Superseder:
Assigned To: Nosy List: BreamoreBoy, Jason.Baker, eric.araujo, ncoghlan, piotr.dobrogost, r.david.murray, rhettinger
Priority: normal Keywords: patch

Created on 2011-01-25 22:32 by Jason.Baker, last changed 2013-10-04 09:34 by piotr.dobrogost. This issue is now closed.

Files
File name Uploaded Description Edit
functools.patch Jason.Baker, 2011-01-25 22:32 Patch for adding misc functions to functools review
Messages (10)
msg127062 - (view) Author: Jason Baker (Jason.Baker) Date: 2011-01-25 22:32
I've created a patch that adds some common functional programming tools to functools.  I've made the patch to work against Python 3.2, but that may be a bit aggressive.  If so, then I can adapt it to work with 3.3.

I also wouldn't be opposed to writing some of these in C if there's a performance need.

The functions I added are:

* flip - flip the first two arguments of a function
* const - return a function that always returns the same thing
* compose - compose multiple functions together
* identity - returns what is passed in to it
* trampoline - calls a function and then calls any returned functions.
msg127066 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2011-01-25 23:00
Some of these have been proposed and rejected before.

Compose has a problematic API because the traditional order of application in mathematics is counter-intuitive for many people.

Const seems reasonable except that we already have ways to do it:  
     twos = lambda :2
     twos = itertools.repeat(2).__next__
The principal use case for const is with map() and itertools.repeat() is already targeted at that use-case:
     cubes = list(map(pow, range(10), repeat(3)))

Flip would be nice on occasion:
     colorseq = list(map(flip, enumerate(
       'red orange yellow green blue indigo violet'.split())))

Identity is problematic for a few reasons.  First, there doesn't seem to be one signature that fits all use cases:
     identity1 = lambda *args:  *args   # always returns a tuple
     identity2 = lambda arg:  arg       # always returns a scalar
Also, the availability of 'identity' seems to encourage bad design.
Speedwise, it is usually better to have two paths (predicate is None vs some given predicate) than have an identity function default (predict=identity).  See the itertools module for examples.

Trampoline is interesting, but the use case doesn't seem to arise much in Python programming and when it does, it is usually clearer to write:
    for f in get_actions():
        f()
That straight-forward code is superior, not just because of its readability but also because it is easily modified to handle return values being feed in to consecutive calls, adding optional and keyword arguments, introducing logging, etc.  

In short, some of these constructs are better suited for languages that a more purely functional in nature.  Those don't treat scalar arguments differently than multiple arguments, those don't use keywords or optional arguments, currying can be done cleanly etc.

Experiences with the itertools module have shown than not all of the usual favorite functional gizmos fit well in the Python language.  They are tempting toys, but typically don't beat regular, clean Python code.

One last comment, any time a group of interoperative tools is proposed, I think it absolutely necessary to construct a bunch of sample code to exercise the APIs working in concert with one another.  With the itertools module, considerable effort was directed at designing a toolkit with cleanly interoperative parts.

Also, when proposing something that changes the fundamentals of how people would design their programs, I think it is necessary to scan real-world code to find many examples where the code would be much improved if it used the new constructs.  (This is similar to the notion of a Phase III clinical trial -- even if something works, it needs to be shown to be better than existing alternatives).
msg127067 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2011-01-25 23:08
One other thought:  The use of list comprehensions (a.k.a. list displays) and generator expressions has made many functional tools less necessary than ever.

   # preferred over map(pow, repeat(2), range(5))
   [pow(2, x) for x in range(5)]

   # preferred over map(flip, dict.items())
   [(v, k) for k, v in mydict.items()]

The list comprehension and genexp styles are much more flexible and works well with default arguments, constants, and using a combination of features:

   # hard to do with functionals
   [f(g(2,x), h(x, 3, y, flag=True)) for x, y in zip(X, Y) if x>y//2]
msg127076 - (view) Author: Jason Baker (Jason.Baker) Date: 2011-01-26 00:34
Ray,  thanks for prompt and thorough feedback.  To address your concerns:

* I'm fine with doing away with const and identity (long story short I haven't really used them in functional languages anyway).  There were reasons for defining identity the way it is, but it's hardly a big deal if that doesn't make it in.
* Personally, I see the mathematical notation for compose to be very intuitive, however it's typically described in a non-intuitive manner.  The functions get called in a left-to-right manner (as is intuitive), however, when you see it defined it is usually in a right-to-left manner.  Thus it can be confusing because we say compose(f, g)(x) [left-to-right]  is equivalent to g(f(x)) [which reads right-to-left, but is still doing what you might expect intuitively].  But I doubt most people that use Python will care about the mathematical usage anyway.  What if we called  the function "pipeline"?  That's essentially what it's doing, and I think people would find that easier to understand.
* I'm not saying you're wrong about trampoline, but I want to make sure that you and I are discussing the same thing.  :-)  The idea for trampoline comes from clojure, which is unique among functional languages in that it has no tail-call recursion.  Trampoline is a way to write a function that is tail recursive, but won't blow the stack.  So your example, while similar isn't *quite* the same thing.  The documentation I wrote shows an example function count_to_a_million.  If I wrote it like this I'd blow the stack:

    def count_to_a_million(i):
        if i < 1000000:
            return count_to_a_million(i)
        else:
            return i

Of course, you could definitely argue that it would just be easier to use a loop (or even better range).  However, many functional programmers' brains have just been wired to use recursion which they might find more intuitive as a means of iteration.  Of course, one could argue that it would be better for them to learn the Pythonic way of doing things, and I would agree.  Python is a multi-paradigm language that supports both functional and imperative approaches, therefore it's Pythonic to provide a functional approach.
* It sounds like you feel flip is useful, but it's a matter of whether you feel it's useful enough to add to the standard library.

Lastly, I love list comprehensions and see where you're coming from, but why do the two have to be mutually exclusive?  I mean, the idea for them came from Haskell which hasn't done away with them.  For instance, take your example:

    [f(g(2,x), h(x, 3, y, flag=True)) for x, y in zip(X, Y) if x>y//2]

I certainly hope you don't really write code like that.  :-)

I think something like this is more readable:

    fg = partial(compose(f, g), 2)
    h1 = lambda x, y:  h(x, 3, y, flag=True)
    [(fg(x), h1(x, y)) for x, y in zip(X, Y) if x>y//2]

...but maybe I'm weird.

If you're still opposed to adding these functions, let me know.  I feel that they have use-cases in the Python standard library, but I can add them to pysistence when I port it to Python 3 and make it a functional data structure/algorithm library.  :-)
msg127085 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-01-26 02:42
For flip, const and identity I agree there are already better ways to handle them using either itertools or comprehension syntax.

The problem I have with trampoline is that it depends on the function's *return values* being defined in a certain way (i.e. returning a zero-argument callable instead of just calling them).

The other major factor limiting the appeal of some of the more complex functional programming tools (the one that almost lead to lambda's removal in 3.x) is the availability of nested function definitions.

To rephrase Raymond's last example in more idiomatic Python:

def fg2(x):
  # Comment or docstring explaining fg2
  return f(g(2,x)

def h2(x, y):
  # Comment or docstring explaining h2
  return h(x, 3, y, flag=True)

result = [fg2(x), h2(x, y) for x, y in zip(X, Y) if x>y//2]

I see compose as being the most potential useful, as it would allow us to provide a C accelerated version of the following alternative to trampoline:

def compose(*args):
  def _composed(x):
    for f in args:
      x = f(x)
    return x
  return _composed

While this actually does match the mathematical definition, I agree pointing that out is more confusing than helpful for many people (since the "inside-out" nested evaluation ends up looking like "right-to-left" evaluation when written down)

Raymond's proposed alternative to trampoline could then be written:

  compose(*get_actions())(x)
msg127088 - (view) Author: Jason Baker (Jason.Baker) Date: 2011-01-26 03:52
I'm not sure I understand how Raymond's alternative for trampoline works.  Let's take the factorial algorithm from wikipedia's page on tail recursion[1].  I've implemented the tail recursive version of the algorithm in Python using trampoline:

    from functools import trampoline, partial

    def factorial(n):
        def fact(i, acc):
            if i:
                return partial(fact, (i-1), (acc * i))
            else:
                return acc
        return trampoline(fact, n, 1)

    >>> factorial(5)
    120


How would I implement this using Raymond's alternative?

[1] http://en.wikipedia.org/wiki/Tail_call#Example_programs
msg127102 - (view) Author: Nick Coghlan (ncoghlan) * (Python committer) Date: 2011-01-26 11:58
How the conversion from a recursive algorithm to an iterative one works depends on the specific algorithm involved. A trampoline does the job for tail calls, but not necessarily any other recursive algorithm.

Factorial actually has a fairly trivial iterative algorithm, so it isn't a great example for general purpose algorithm conversion:

def factorial(x):
  result = 1
  for i in range(1, x+1):
    result *= i
  return result

I believe having trampoline in functools would end up being something of an attractive nuisance - in cases where it applies, there are probably better, algorithm specific, ways of eliminating a recursive call.
msg127128 - (view) Author: Raymond Hettinger (rhettinger) * (Python committer) Date: 2011-01-26 19:02
I'm intrigued by the tampoline() but after reading Nick's post, I think it needs to be an external recipe or blog post with extensive examples so that it can mature and either prove its worth or serve as an intellectually stimulating toy.
msg189046 - (view) Author: Mark Lawrence (BreamoreBoy) * Date: 2013-05-12 16:49
To summarize flip, const and identity won't happen, trampoline needs an external recipe or blog post and compose is the only one that's likely to happen.  Opinions please gentlemen.
msg189585 - (view) Author: R. David Murray (r.david.murray) * (Python committer) Date: 2013-05-19 13:01
By my reading compose was also rejected, so I'm going to close this.  (See issue 1506122 for one previous rejection of compose.)
History
Date User Action Args
2013-10-04 09:34:57piotr.dobrogostsetnosy: + piotr.dobrogost
2013-05-19 13:01:43r.david.murraysetstatus: open -> closed

type: enhancement

nosy: + r.david.murray
messages: + msg189585
resolution: rejected
stage: resolved
2013-05-12 16:49:16BreamoreBoysetnosy: + BreamoreBoy
messages: + msg189046
2011-01-26 19:02:03rhettingersetnosy: rhettinger, ncoghlan, eric.araujo, Jason.Baker
messages: + msg127128
2011-01-26 11:58:40ncoghlansetnosy: rhettinger, ncoghlan, eric.araujo, Jason.Baker
messages: + msg127102
2011-01-26 03:52:55Jason.Bakersetnosy: rhettinger, ncoghlan, eric.araujo, Jason.Baker
messages: + msg127088
2011-01-26 02:42:00ncoghlansetnosy: + ncoghlan
messages: + msg127085
2011-01-26 00:34:06Jason.Bakersetmessages: + msg127076
2011-01-25 23:11:29eric.araujosetnosy: + eric.araujo
2011-01-25 23:08:01rhettingersetmessages: + msg127067
2011-01-25 23:00:56rhettingersetnosy: + rhettinger

messages: + msg127066
versions: - Python 3.2
2011-01-25 22:32:21Jason.Bakercreate