So I was inspired (distracted) by the python functional programming module, and got to thinking couldn't things like partial application and function composition be a bit more natural in the syntax.
So it turns out that with decorators and a bit of evil introspection I came up with something that works (at least for functions that don't have keyword arguments). So you can do something like:
@Functional
def add(a, b): return a + b
add3 = add.add
assert(add3(1)(2)(3) == add3(1, 2, 3) == add3(1, 2)(3) == add3(1)(2, 3) == 6)
So what is the magic (or evilness)? Well first we (ab)use decorators to decorate things as Functional, which is you probably know, is just a shortcut for function = Functional(function).
The evil is in the Functional class:
class Functional:
def __init__(self, fn, nargs = None):
self.fn = fn
if not nargs:
_args, _, _, _ = inspect.getargspec(self.fn)
self.nargs = len(_args)
else:
self.nargs = nargs
def __call__(self, *args, **kargs):
if len(args) > self.nargs:
res = self.fn(*args[:self.nargs], **kargs)
if res.__class__ == Functional:
return res(*args[self.nargs:])
if type(res) != type((0,)):
res = (res, )
return res + args[self.nargs:]
elif len(args) == self.nargs:
return self.fn(*args, **kargs)
else:
def newfunc(*fargs, **fkeyw):
return self.fn(*(args + fargs))
newfunc.func = self.fn
newfunc.args = args
return Functional(newfunc, self.nargs - len(args))
def __getattr__(self, name):
if hasattr(self.fn, name):
return getattr(self.fn, name)
func_1 = self.fn
func_2 = globals()[name]
def composition(*args, **kwargs):
res = func_2(*args, **kwargs)
if type(res) != type((0,)):
res = (res, )
return self(*res)
return Functional(composition, func_2.nargs)
I totally abuse the __getattr__ method so that dot becomes a composition operator. This returns a new function (which is also Functional), which when called will pass on the return value from the first function to the second function. If the first function returns multiple results each of these is passed as arguments to the second function.
The real magic comes in the overload __call__, which is where
partial functions are hacked in. Basically if not enough arguments are
passed to the function is returns a new function that accumulates the
arguments already passed and once it gets enough calls the original function.
Of course the function returned from partial application is itself. The real
evil is in supporting the case add3(1, 2, 3) which means
we detect if too many arguments are passed and then call with only the first
arguments, then if the called function returns another functional we apply the remaining
arguments to it.
Oh yeah, I wouldn't use this in any real python code, as it is likely to confuse everyone!