Last active
December 17, 2017 16:36
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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