Charles R
Charles R

Reputation: 19089

Better Function Composition in Python

I work in Python. Recently, I discovered a wonderful little package called fn. I've been using it for function composition.

For example, instead of:

baz(bar(foo(x))))

with fn, you can write:

(F() >> foo >> bar >> baz)(x) .

When I saw this, I immediately thought of Clojure:

(-> x foo bar baz) .

But notice how, in Clojure, the input is on the left. I wonder if this possible in python/fn.

Upvotes: 11

Views: 3912

Answers (7)

Holomorphic Guy
Holomorphic Guy

Reputation: 86

I understand what you mean. It doesn't make sense. In my opinion this python library does it better.

>>> from compositions.compositions import Compose
>>> foo = Compose(lambda x:x)
>>> foo = Compose(lambda x:x**2)
>>> foo = Compose(lambda x:sin(x))
>>> (baz*bar*foo)(x)

Upvotes: 0

christian wuensche
christian wuensche

Reputation: 1043

My compose function that returns a function

def compose(*args):
    length = len(args)
    def _composeInner(lastResult, index):
        if ((length - 1) < index):
            return lastResult
        return _composeInner(args[index](lastResult), index + 1)

    return (lambda x: _composeInner(x, 0))

Usage:

fn = compose(
        lambda x: x * 2,
        lambda x: x + 2,
        lambda x: x + 1,
        lambda x: x / 3
    )

result = fn(6) # -> 5

Upvotes: 0

Ana Nimbus
Ana Nimbus

Reputation: 735

This seems to work for simple input. Not sure it is worth the effort for complex input, e.g., ((42, 'spam'), {'spam': 42}).

def compose(function, *functions):
    return function if not functions else \
            lambda *args, **kwargs: function(compose(*functions)(*args, **kwargs))

def rcompose(*functions):
    return compose(*reversed(functions))

def postfix(arg, *functions):
    return rcompose(*functions)(arg)

Example:

>>> postfix(1, str, len, hex)
'0x1'
>>> postfix(1, hex, len)
3

Upvotes: 0

fl00r
fl00r

Reputation: 83680

I came up with this

def _composition(arg, *funcs_and_args):
    """
    _composition(
        [1,2,3], 
        (filter, lambda x: x % 2 == 1), 
        (map, lambda x: x+3)
    )
    #=> [4, 6]
    """
    for func_and_args in funcs_and_args:
        func, *b = func_and_args
        arg = func(*b, arg)
    return(arg)

Upvotes: 0

MisterMetaphor
MisterMetaphor

Reputation: 6008

If you want to use fn, with a little hack you can get a bit closer to Clojure syntax:

>>> def r(x): return lambda: x
>>> (F() >> r(x) >> foo >> bar >> baz)()

See how I added another function at the beginning of the composition chain that will just return x when called. The problem with this is that you still have to call your composed function, just without any arguments.

I think @Blender's answer is your best bet trying to emulate Clojure's thread function in Python.

Upvotes: 1

BrenBarn
BrenBarn

Reputation: 251355

You can't get that exact syntax, although you can get something like F(x)(foo, bar, baz). Here's a simple example:

class F(object):
    def __init__(self, arg):
        self.arg = arg

    def __call__(self, *funcs):
        arg = self.arg
        for f in funcs:
            arg = f(arg)
        return arg

def a(x):
    return x+2
def b(x):
    return x**2
def c(x):
    return 3*x

>>> F(2)(a, b, c)
48
>>> F(2)(c, b, a)
38

This is a bit different from Blender's answer since it stores the argument, which can later be re-used with different functions.

This is sort of like the opposite of normal function application: instead of specifying the function up front and leaving some arguments to be specified later, you specify the argument and leave the function(s) to be specified later. It's an interesting toy but it's hard to think why you'd really want this.

Upvotes: 2

Blender
Blender

Reputation: 298106

You can't replicate the exact syntax, but you can make something similar:

def f(*args):
    result = args[0]

    for func in args[1:]:
        result = func(result)

    return result

Seems to work:

>>> f('a test', reversed, sorted, ''.join)
' aestt'

Upvotes: 11

Related Questions