Skip to content

Instantly share code, notes, and snippets.

@badocelot
Last active December 17, 2017 16:36
Show Gist options
  • Save badocelot/6ed0e67d643a9be0a1f9 to your computer and use it in GitHub Desktop.
Save badocelot/6ed0e67d643a9be0a1f9 to your computer and use it in GitHub Desktop.
Support for some aspects of functional programming (argument piping, composition, and currying) for Python
import functools
from inspect import _empty, signature
class Function:
"""Decorator class for callables, making them curried and composable."""
class Recursor:
"""Wrapper class for identifying tail recursive calls to trampoline them."""
def __init__(self, fn):
"""Initialize the instance."""
self.__fn = fn
self.__args = ()
def __call__(self, *args):
"""Capture the arguments for application by the trampoline."""
self.__args = args
return self
@property
def args(self):
"""Access the captured arguments."""
return self.__args
def __init__(self, fn, argc=None, argv=(), is_recursive=False):
"""Initialize an instance.
Required arguments:
fn -- the callable object to be decorated
Optional arguments:
argc -- the number of arguments required by the callable object
argv -- a tuple of arguments to be applied when this object is called
is_recursive -- whether the function should supply a Recursor as its
first argument
Exceptions:
Raises TypeError if fn is not callable.
Raises ValueError if argc cannot be deduced from fn.
Raises TypeError if argv is not a tuple.
"""
if not callable(fn):
raise TypeError("argument 'fn' must be callable")
elif isinstance(fn, Function):
# copy the existing object
self.__fn = fn.__fn
self.__argc = fn.__argc if argc is None else argc
self.__argv = fn.__argv + tuple(argv)
self.__is_recursive = fn.__is_recursive or bool(is_recursive)
else:
self.__fn = fn
if argc is None:
sig = signature(self.__fn)
# count the arguments that don't have default values
self.__argc = sum(int(p.default != _empty)
for p in sig.parameters.values())
else:
self.__argc = argc
self.__argv = tuple(argv)
self.__is_recursive = bool(is_recursive)
def __call__(self, *args):
"""Apply the supplied arguments to the function."""
all_args = self.__argv + args
if len(all_args) >= self.__argc:
if self.__is_recursive:
# trampoline
rec = Function.Recursor(self.__fn) # create the recursor
result = self.__fn(rec, *all_args)
# loop while the recursor is being returned (tail call)
while result is rec:
result = self.__fn(rec, *rec.args)
return result
else:
return self.__fn(*all_args)
else:
# capture the new arguments and return the partially-applied
# function
return Function(self, argv=all_args)
def compose(self, other):
"""Composes this function with other."""
if not isinstance(other, Function):
other = Function(other)
def composed_function(*args):
return self(other(*args))
return Function(composed_function, other.__argc - len(other.__argv))
def rcompose(self, other):
"""Composes other with this function."""
if not isinstance(other, Function):
other = Function(other)
def composed_function(*args):
return other(self(*args))
return Function(composed_function, self.__argc - len(self.__argv))
def __lshift__(self, other):
"""self << other"""
return self.compose(other)
def __rlshift__(self,other):
"""other << self"""
return self.rcompose(other)
def __rshift__(self, other):
"""self >> other"""
return self.rcompose(other)
def __rrshift__(self, other):
"""other >> self"""
return self.compose(other)
def __lt__(self, val):
"""self < val"""
return self(val)
def __rgt__(self, val):
"""val > self"""
return self(val)
def __gt__(self, fn):
"""self > fn"""
return fn(self)
def __rlt__(self, fn):
"""fn < self"""
return fn(self)
def __mod__(self, val):
"""self % fn"""
return self(val)
def __rmod__(self, fn):
"""fn % self"""
return fn(self)
@property
def number_of_args(self):
return self.__argc
@property
def applied_args(self):
return self.__argv
def __repr__(self):
return repr(self.__fn)
# convenience functions for decorating
def fun(fn=None, argc=None, argv=(), is_recursive=False):
"""Convenience function for decorating a callable object with Function."""
if fn is not None:
return Function(fn, argc, argv, is_recursive)
else:
return lambda fn: Function(fn, argc, argv, is_recursive)
def fun_rec(fn=None, argc=None, argv=()):
"""Convenience function for decorating a callable object with Function,
setting is_recursive to True."""
if fn is not None:
return Function(fn, argc, argv, True)
else:
return lambda fn: Function(fn, argc, argv, True)
# define some functional builtin replacements
abs = fun(abs, 1)
all = fun(all, 1)
any = fun(any, 1)
ascii = fun(ascii, 1)
bin = fun(bin, 1)
callable = fun(callable, 1)
isinstance = fun(isinstance, 2)
map = fun(map, 2)
max = fun(max, 1)
memoryview = fun(max, 1)
min = fun(min, 1)
reduce = fun(functools.reduce, 2)
zip = fun(zip, 0)
# WARNING: types become noninheritable if you import these
bool = fun(bool, 0)
bytearray = fun(bytearray, 0)
bytes = fun(bytes, 0)
complex = fun(complex, 0)
dict = fun(dict, 0)
float = fun(float, 0)
int = fun(int, 0)
list = fun(list, 0)
set = fun(set, 0)
tuple = fun(tuple, 0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment