Let's create curried functions in Python!
You can search for curried functions on the internet and you will find very scary things Wiki. Some languages like Haskell have this functionality defined at the core and its something everyone plays on his favorite language when they enter the functional world.
Let's define what we want, not why we want it, because that's not important, right? We all want curried functions!
Curried function
A curried function is a function that only receives one argument.
def add2(x):
return x + 2
But, what if we want to create a function that receives more than one argument? Well, curried functions can return any value, including another function, preferably a curried one.
def add(x):
def add_x(y):
return x + y
return add_x
But, what if we want to add even more arguments? It gets pretty ugly when the number of arguments that our function needs increases.
Let's try to deal with this with a more general implementation.
How do we use this function? Now the syntax for a function call is a little bit different and it can be strange. On the other hand, we now can get one "partially" evaluated function.
add(2)(3) # 5
add2 = add(2) # function
add2(3) # 5
This comes handy when we use map
or filter
def greater_than(x):
def greater_than_x(y):
return y > x
return greater_than_x
list(
map(
add(2),
filter(
greater_than(5),
range(10)
)
)
) # [8, 9, 10, 11]
Ideally, we want to keep the nice syntax for function calls that Python has when we don't want to use the curried possibility.
Proposal
Let's define a decorator that takes a normal python function and returns a curried version of it. We will define our function as usual and we will get both ways of calling it.
@curried
def add(x, y):
return x + y
add2 = add(2) # function
add2(3) # 5
add(2, 3) # 5
This is a possible implementation of this curried
decorator.
from inspect import signature, Parameter
def curried(func):
sig = signature(func)
params = sig.parameters.values()
assert all(map(is_positional, params)), \
'All parameters should be positional'
def curried_func_first_call(*args):
N = len(params)
applied = list(args)
def curried_func(*args):
assert len(applied) + len(args) <= N, \
'Too many arguments, expected {}'.format(N)
applied.extend(args)
if len(applied) == N:
return func(*applied)
return curried_func
if len(applied) == N:
return func(*applied)
return curried_func
return curried_func_first_call
def is_positional(param):
return param.kind == Parameter.POSITIONAL_ONLY or \
param.kind == Parameter.POSITIONAL_OR_KEYWORD
What about partial
Probably everyone has heard about partial
function from the module functools
. But this does not behave entirely as we want. It does a lot more than we are doing here, but I was interested in syntax.
One of the problems is that partial
always returns a function, even when all the arguments were supplied.
def add(x, y):
x + y
f = partial(add, 2, 3) # function
f() # 5
Furthermore, partial
does not curry the resulting function
def add(x, y, z):
return x + y + z
f = partial(add, 1)
f(2) # TypeError: add() missing 1 required positional argument: 'z'
f(2, 3) # 6
Top comments (0)