19

Some time ago I looked over Haskell docs and found it's functional composition operator really nice. So I've implemented this tiny decorator:

from functools import partial

class _compfunc(partial):
    def __lshift__(self, y):
        f = lambda *args, **kwargs: self.func(y(*args, **kwargs)) 
        return _compfunc(f)

    def __rshift__(self, y):
        f = lambda *args, **kwargs: y(self.func(*args, **kwargs)) 
        return _compfunc(f)

def composable(f):
    return _compfunc(f)

@composable    
def f1(x):
    return x * 2

@composable
def f2(x):
    return  x + 3

@composable
def f3(x):
    return (-1) * x

@composable
def f4(a):
    return a + [0]

print (f1 >> f2 >> f3)(3) #-9
print (f4 >> f1)([1, 2]) #[1, 2, 0, 1, 2, 0]
print (f4 << f1)([1, 2]) #[1, 2, 1, 2, 0]

The problem: without language support we can't use this syntax on builtin functions or lambdas like this:

((lambda x: x + 3) >> abs)(2)

The question: is it useful? Does it worth to be discussed on python mail list?

si14
  • 783
  • 6
  • 11
  • ...but it is still possible to write something like this: print (composable(lambda x: x + 3) >> composable(abs))(-4) – si14 Feb 17 '10 at 20:52
  • For mathematical functions usually is used * for composition and + for summation of functions, such that (f * g + h)(x) = f(g(x)) + h(x). And also the power operator can be used: f**2(x) = f(f(x)) etc... – nadapez Apr 30 '20 at 22:49

4 Answers4

8

IMHO: no, it's not. While I like Haskell, this just doesn't seem to fit in Python. Instead of (f1 >> f2 >> f3) you can do compose(f1, f2, f3) and that solves your problem -- you can use it with any callable without any overloading, decorating or changing the core (IIRC somebody already proposed functools.compose at least once; I can't find it right now).

Besides, the language definition is frozen right now, so they will probably reject that kind of change anyway -- see PEP 3003.

Cat Plus Plus
  • 125,936
  • 27
  • 200
  • 224
  • compose() is not so clear. What is the direction of composition? You have to look at documentation to answer this question. – si14 Feb 17 '10 at 15:24
  • 3
    @si14: Not knowing Haskell I don't find >> and << that clear either (does it mean insert or wrap the other function?). – nikow Feb 17 '10 at 15:32
  • 1
    @Nikow: I don't know it too, but >> and << clearly define the "computation flow": f1 >> f2 >> f3 means "put result of f1 to f2, then result of f2 to f3". Alternative is f1(f2(f3(x))) with a lot of messy braces. – si14 Feb 17 '10 at 15:35
  • 1
    @si14: I see the logic in that, but still think that it is mostly a matter of habit. There is at least one functional language that makes excessive use of parentheses ;-) To me this sounds similar to the (Java-)people complaining that len(a) is not object oriented and should be a.len(). – nikow Feb 17 '10 at 15:43
  • `>>` means something importantly different in Haskell. It's a special case of `>>=` which includes a kind of function composition, but also unpacking from a Monad. @si14: In fact I was assuming you meant f1>>f2 to be lambda x: f2(f1(x)). I see now why you construe it the way you do, but this is the opposite order as the mathematically familiar composition operator. – dubiousjim Feb 17 '10 at 15:44
  • I know, but Python doesn't have |> operator so I redefined these. – si14 Feb 17 '10 at 15:47
  • @nikow: this .len() thing is about design and >> is about syntax sugar. Quite different things :) – si14 Feb 17 '10 at 15:48
  • I think that |> operator is not very readable. Python is not for every task. There are some things that Haskell and F# are better at. – Hamish Grubijan Feb 17 '10 at 15:53
  • I understand that Python is not a silver bullet. But in some cases this syntax would be nice IMHO. – si14 Feb 17 '10 at 16:00
  • @si14: NO, `len(a)` in Python is syntactic sugar for `a.len()` (or more precisely `a.__len__()`). – nikow Feb 17 '10 at 16:08
  • @si14 please clarify that you meant `f1 >> f2 >> f3` to be equivalent to `lambda x : f3(f2(f1(x)))`; after all, you were saying that the result of `f1` would be fed to `f2`, etc. It seems you made a typo in your comment. I want to clarify this since if anyone in the world interprets `>>` in the opposite direction, I want to know that (in which case I'll quadruple the amount of comments related to `__rshift__` in my own code, where I also use it to mean composition). – max Oct 25 '12 at 07:26
6

Function composition isn't a super-common operation in Python, especially not in a way that a composition operator is clearly needed. If something was added, I am not certain I like the choice of << and >> for Python, which are not as obvious to me as they seem to be to you.

I suspect a lot of people would be more comfortable with a function compose, the order of which is not problematic: compose(f, g)(x) would mean f(g(x)), the same order as o in math and . in Haskell. Python tries to avoid using punctuation when English words will do, especially when the special characters don't have widely-known meaning. (Exceptions are made for things that seem too useful to pass up, such as @ for decorators (with much hesitation) and * and ** for function arguments.)

If you do choose to send this to python-ideas, you'll probably win a lot more people if you can find some instances in the stdlib or popular Python libraries that function composition could have made code more clear, easy to write, maintainable, or efficient.

Mike Graham
  • 73,987
  • 14
  • 101
  • 130
  • 1
    Can you please clarify why `>>` and `<<` are not obvious? I'm not disagreeing, I'm just trying to see if there's consensus among programmers. – max Oct 25 '12 at 07:28
  • @max, I have no idea how one describes why something is _not obvious_. There's simply no reason that I would look at `<<` and `>>` and say, "Ah, they should be used for this!" – Mike Graham Oct 25 '12 at 16:17
  • @max an attempt: it's *less obvious* due to *no tradition* of that usage. Also *less obvious* due that *in a different sense* `>>` is used in extended print statement ('print chevron'). Also *not obvious* to tie [shifting operations](https://docs.python.org/2/reference/expressions.html#shifting-operations) of [special functions](https://docs.python.org/2/reference/datamodel.html#emulating-numeric-types) to composition for *all* [callables](https://docs.python.org/2/reference/datamodel.html#emulating-callable-objects): *it's unclear which kinds of objects would this disqualify*. – n611x007 May 09 '14 at 12:40
  • @max: also: [Explicit is better than implicit](http://legacy.python.org/dev/peps/pep-0020/). to what degree is this explicit: `(f1 >> f2 << f3 >> f4)(3)`? – n611x007 May 09 '14 at 12:45
  • @naxa, that's not obvious, but I can't tell what's implicit about it. – Mike Graham May 10 '14 at 13:59
6

You can do it with reduce, although the order of calls is left-to-right only:

def f1(a):
    return a+1

def f2(a):
    return a+10

def f3(a):
    return a+100

def call(a,f):
    return f(a)


reduce(call, (f1, f2, f3), 5)
# 5 -> f1 -> f2 -> f3 -> 116
reduce(call, ((lambda x: x+3), abs), 2)
# 5
n611x007
  • 8,952
  • 8
  • 59
  • 102
1

I don't have enough experience with Python to have a view on whether a language change would be worthwhile. But I wanted to describe the options available with the current language.

To avoid creating unexpected behavior, functional composition should ideally follow the standard math (or Haskell) order of operations, i.e., f ∘ g ∘ h should mean apply h, then g, then f.

If you want to use an existing operator in Python, say <<, as you mention you'd have a problem with lambdas and built-ins. You can make your life easier by defining the reflected version __rlshift__ in addition to __lshift__. With that, lambda/built-ins adjacent to composable objects would be taken care of. When you do have two adjacent lambda/built-ins, you'll need to explicitly convert (just one of) them with composable, as @si14 suggested. Note I really mean __rlshift__, not __rshift__; in fact, I would advise against using __rshift__ at all, since the order change is confusing despite the directional hint provided by the shape of the operator.

But there's another approach that you may want to consider. Ferdinand Jamitzky has a great recipe for defining pseudo infix operators in Python that work even on built-ins. With this, you can write f |o| g for function composition, which actually looks very reasonable.

max
  • 49,282
  • 56
  • 208
  • 355
  • 1
    to clarify, in the cited framework each function has to be defined as an `Infix` operator, as opposed to langs like Mathematica (and others), where functions can be composed without redefining them. – alancalvitti Jan 07 '20 at 20:55