Last year I wrote a post about function currying in JavaScript, it corrected the (absolutely) wrong implementation that I had written some years ago. That last implementation did not feel particularly easy to understand to me. I was using three functions: curry, one for saving args and the curried function itself... I've recently implemented currying in Python, without looking into the JavaScript version, and it's interesting how thinking more in Python terms has made me better think about a JavaScript implementation
Currying a function means ending up with an invokable/callable object that references the original function and stores the provided parameters until all of them have been provided. In each "incomplete invokation" it has to return another invokable object trapping the expanded list of parameters. In Python an "invokable/callable object with state" is either a closure or an instance of a callable class (well, indeed normal functions are also instances of callables). And the creator of that invokable object is either a closure factory or a callable class. For my implementation I've used a Callable rather than a closure, somehow this time it felt more intutive:
class Curried:
def __init__(self, fn, args: list[Any] | None = None):
# in the initial call to create the initial curried function args is None
self.fn = fn
self.saved_args = args or []
self.expected_args_len = len(inspect.signature(self.fn).parameters)
# notice how for class based decorators we have to use update_wrapper here, rather than wraps
functools.update_wrapper(self, fn)
def __call__(self, *args):
current_args = [*self.saved_args, *args]
if len(current_args) > self.expected_args_len:
raise Exception("too many arguments!!!")
if len(current_args) == self.expected_args_len:
return self.fn(*current_args)
else:
return Curried(self.fn, current_args)
# alias for better semantics when used as decorator
curry = Curried
def test_curried():
def format_city(planet: str, continent: str, country: str, region: str, city: str) -> str:
"""I'm the format_city docstring"""
return f"{planet}.{continent}.{country}.{region}_{city}"
curried_format_city = Curried(format_city)
# or if used as decorator:
# @curry
# def format_city(planet: str, continent: str, country: str, region: str, city: str) -> str:
# """I'm the format_city docstring"""
# return f"{planet}.{continent}.{country}.{region}_{city}"
print(curried_format_city("Earth")("Europe", "Spain")("Asturies", "Xixon"))
format1 = curried_format_city("Earth", "Europe")
format2 = curried_format_city("Earth", "Asia")
# update_wrapper works nicely
print(f"{format1.__name__=}, {format1.__doc__=}, ")
print(format1("Spain")("Asturies", "Xixon"))
print(format1("France")("Ile de France", "Paris"))
print(format2("China")("Beijing", "Beijing"))
print(format2("China")("Guandong", "Shenzen"))
format3 = format2("Russia")("Northwestern")
print(f"{format3.__name__=}, {format3.__doc__=}, ")
print(format3("Saint Petersburg"))
# Earth.Europe.Spain.Asturies_Xixon
# format1.__name__='format_city', format1.__doc__="I'm the format_city docstring",
# Earth.Europe.Spain.Asturies_Xixon
# Earth.Europe.France.Ile de France_Paris
# Earth.Asia.China.Beijing_Beijing
# Earth.Asia.China.Guandong_Shenzen
# format3.__name__='format_city', format3.__doc__="I'm the format_city docstring",
# Earth.Asia.Russia.Northwestern_Saint Petersburg
As you see, a curried function is a callable object, an instance of the Curried class (that has a __call__ method). As I said, the curried function is equivalent to a closure, and the Curried class is equivalent to a closure factory. Notice that as I'm creating a callable object rather than a standard function I'm using functools.update_wrapper (rather than functools.wraps) to set the original __name__, __doc__, etc in the curried callable. I can invoke it directly (Curried(fn)) or use it as a decorator at function definition time.
In my previous JavaScript implementation I had 3 elements: a curry function, a saveArgs function and the closure itself. That's why it felt a bit strange to me, following the Python implementation, I only need 2 elements, the closure factory and the closure. So here it goes my new JavaScript implementation:
let curry = function createCurriedFn(fn, args) {
// createCurriedFn is a closure factory, creates a closure that traps original fn and parameters
let savedArgs = args ?? [];
// return the curriedFn/closure
return (...args) => {
const curArgs = [...savedArgs, ...args];
return curArgs.length >= fn.length
? fn(...curArgs)
: createCurriedFn(fn, curArgs);
};
}
curriedFormat = curry(formatMessages);
curriedFormat("a")("b")("c");
curriedFormat("d")("e")("f");
curriedFormat("g", "h")("i");
curriedFormat("j", "k", "l");
// a-b-c
// d-e-f
// g-h-i
// j-k-l
As we know Python features named parameters (contrary to JavaScript), so we should contemplate that in our curry function. This is the improved version that does just that:
class Curried:
def __init__(self, fn, args: list[Any] | None = None, kwargs: dict[str, Any] | None = None):
# in the initial call to create the initial curried function args is None
self.fn = fn
self.saved_args = args or []
self.saved_kwargs = kwargs or {}
self.expected_args_len = len(inspect.signature(self.fn).parameters)
# notice how for class based decorators we have to use update_wrapper here, rather than wraps
functools.update_wrapper(self, fn)
def __call__(self, *args, **kwargs):
current_args = [*self.saved_args, *args]
current_kwargs = {**self.saved_kwargs, **kwargs}
if (cur_len := (len(current_args) + len(current_kwargs))) > self.expected_args_len:
raise Exception("too many arguments!!!")
if cur_len == self.expected_args_len:
return self.fn(*current_args, **current_kwargs)
else:
#return wraps(self.fn)(Curried(self.fn, cur_arguments))
return Curried(self.fn, current_args, current_kwargs)
# alias for better semantics when used as decorator
curry = Curried
def test_curried():
def format_city(planet: str, continent: str, country: str, region: str, city: str) -> str:
"""I'm the format_city docstring"""
return f"{planet}.{continent}.{country}.{region}_{city}"
curried_format_city = Curried(format_city)
#print(curried_format_city.__name__)
print(curried_format_city("Earth")("Europe", "Spain")("Asturies", "Xixon"))
format1 = curried_format_city("Earth", "Europe")
format2 = curried_format_city("Earth", "Asia")
# update_wrapper works nicely
print(f"{format1.__name__=}, {format1.__doc__=}, ")
print(format1("Spain")(region="Asturies", city="Xixon"))
print(format1("France")("Ile de France", city="Paris"))
print(format2(country="Chinaaa")(country="China")(city="Guangzhou", region="Guangdong"))
print(format2("China")("Guandong", "Shenzen"))
print(format2("China")("Guangdong", "Guangzhou"))
print(format2("China")("Guandong", city="Shenzen"))
format3 = format2("Russia")("Northwestern")
print(f"{format3.__name__=}, {format3.__doc__=}, ")
print(format3("Saint Petersburg"))
test_curried()
print("----------------------")
# Earth.Europe.Spain.Asturies_Xixon
# format1.__name__='format_city', format1.__doc__="I'm the format_city docstring",
# Earth.Europe.Spain.Asturies_Xixon
# Earth.Europe.France.Ile de France_Paris
# Earth.Asia.China.Guangdong_Guangzhou
# Earth.Asia.China.Guandong_Shenzen
# Earth.Asia.China.Guangdong_Guangzhou
# Earth.Asia.China.Guandong_Shenzen
# format3.__name__='format_city', format3.__doc__="I'm the format_city docstring",
# Earth.Asia.Russia.Northwestern_Saint Petersburg
Notice that contrary to what happens with standard functions, in the curried functions created by this implementation we can pass unnamed parameters after named ones, but the unnamed ones have to be provided in the same order as in the original function. Same as with functools.partial, we can provide the same named parameter multiple times, each new provided value overwrites the previous one.
No comments:
Post a Comment