Package: yummycurry
FeaturesDecorators for functions, methods and class methods.Supports positional, positional-only, positional-variable,
keyword, keyword-only, and keyword-variable arguments.Accepts too few argument.Accepts too many arguments, storing them for the next resulting function that
wants them.Automatically applies the underlying callable when all the necessary arguments
have been passed.Automatically re-curries/re-applies when the result of the callable is
itself callable.Picklable (no lambdas).Flat (curry(curry(f))is simplified tocurry(f)).Inspection-friendly: implements__signature__,__name__,__str__,
etc.ContextMathematically, Python can be approached as a Closed Monoidal Category.Category: Python’s functions and other callables can be composed.Monoidal: We can put two python things together to form a new python thing
(lists, dictionaries, tuples, class attributes, etc.).Closed: Pythons’s functions (callables) are first-class, meaning they can
be treated like any other non-callable Python thing.All closed monoidal categories are also Cartesian closed categories,
and all Cartesian closed categories have currying.In simple terms, currying takes a function that takes two arguments
and turns it into a function of one argument that returns a function of one
argument:(a, b) -> c  ===curry==>  a -> (b -> c)The opposite operation is called un-currying, and lets you call a function
with more arguments than it requires.Python is not limited to unary functions and pairs of arguments, Python’s
callable objects accept posititional arguments, keyword arguments, and even
variable arguments.
Python has functions, but also methods, some of which can be class methods that
are in fact implemented as descriptors.
Furthermore, Python can do introspection, so not only should curried callables
work, they should also maintain signatures, names, docstrings, etc.
To complicate things even more, we should be able to pickle curried callables
and have weakrefs to them.Theyummycurrypackage aims at providing a simple and reliable way to
automatically curry, uncurry and apply (call) any Python callable.WalkthroughDecorator or simple functionThe functionyummycurry.currycan be used as a decorator or as a function:from yummycurry import curry

def dbl(x):
    return x * 2
dbl = curry(dbl)  # As a function.

@curry  # As a decorator.
def inc(x):
    return x + 1Too few argumentsA trivial use ofcurryis to call a function with fewer arguments than it
requires.We can see it the other way around and design withcurryin mind, in order
to define functions that take more parameters than they actually need.
It is common to see function composition implemented as such:def compose(f, g):
    return lambda x: f(g(x))One severe problem is that lambdas cannot be pickled, which prevents them
from being shared easily in a multiprocessing environment.
Another problem is the lack of useful__doc__and__name__which make
introspection, documentation and printing/logging difficult.
Finally they are difficult to read.
As a rule of thumb, lambdas should not escape the scope in which they are
defined.We can avoid returning lambdas by makingcomposetake a third argument and
relying oncurryto wait for it:@curry
def compose(f, g, x):
    """Composition of unary functions."""
    # No need to return a lambda, ``curry`` takes care of it.
    return f(g(x))

dbl_inc = compose(dbl, inc)
assert dbl_inc(10) == 22

# Function composition is associative: as long as the order or the leaves
# is preserved, the way that the tree forks does not matter.
pipeline_1 = compose(compose(dbl, compose(inc, dbl)), compose(inc, inc))
pipeline_2 = compose(compose(compose(compose(dbl, inc), dbl), inc), inc)
assert pipeline_1(10) == 2 * (1 + 2 * (1 + 1 + 10))
assert pipeline_2(10) == 2 * (1 + 2 * (1 + 1 + 10))This version ofcompose, which relies oncurry, has no lambdas and is
therefore picklable.
We will see later thatcurrypreserves the name, documentation, and even
the signature of the underlying callable.
Those are also features offunctools.partial, soyummycurrybrings no
surprise there.Automatic application, re-currying and uncurryingWithfunctools.partial, there are two explicit phases:The currying phase:
create apartialobject by setting some of, or all, the arguments.The application phase:
apply the partial object by calling it with all the remaining arguments,
even if there are actually no remaining arguments.Example:from functools import partial

def cool(x, y, z):
    return x * 100 + y * 10 + z

p = partial(cool, 1, 2, 3)  # Phase 1: explicit currying.
result = p()  # Phase 2: explicit application, even if there are no arguments.
assert result == 123If we want to curry again we have to be explicit:p = partial(cool, 1)  # Explicit currying.
p = partial(p, 2)  # Explicit currying, again.
result = p(3)  # Explicit application.
assert result == 123Automatic applicationWithyummycurry, function application is automated:p = curry(cool, 1)
p = p(2)
result = p(3)
assert result == 123To achieve this,yummycurryinspects its underlying callable (in our casecool) and compares its signature with the arguments that have been
provided so far.
If the arguments satisfy the signature of the underlying callable, then
it is automatically applied, otherwiseyummycurryreturns a new callable
that waits for more arguments: it re-curries itself.Automatic application stops when the result is not callable.
This means thatcurryaccepts non-callable objects; it just returns
them untouched:s = "Don't call us, we'll call you"
assert curry(s) == s

@curry
def actually_constant():
    return 123

assert actually_constant == 123Automatic re-curryingNot only doesyummycurryre-curries its underlying callable when it needs
more arguments, but it also automatically curries any callable resulting from
its application.If a callablef0returns a callablef1that is not explicitly
curried, thencurry(f0)will automatically curryf1:def f0(x:int):  # Uncurried
    def f1(y:int, z:int) -> int:  # Uncurried
        return x*100 + y*10 + z
    return f1

# Without currying, this is the only thing that works:
assert f0(1)(2, 3) == 123

try:
    assert f0(1)(2)(3) == 123
except TypeError:
    pass  # The result of f0(1) is not curried so f0(1)(2) is incorrect.

# If we curry f0, then its result ``f0(1)`` is automatically curried:
f0 = curry(f0)
assert f0(1)(2)(3) == 123  # Now it works.The process continues: ifcurry(f1)returns a callablef2then it gets
curried as well.
The process stops when the result of a function is not callable.
In this example, the number123is not callable so the automatic
currying and application stops.When currying, we wish to always preservef(x, y) ==f(x)(y).
There are cases in which this symmetry cannot be preserved: whenfaccepts
a variable-argument parameter (like*argsor**kwargs), or when a
parameter has a default value.
This will be addressed later in this document.Automatic uncurryingUnlikefunctools.partialand many other Python packages that ship a currying
function,yummycurryaccepts arguments even when they do not match any
parameter of the curried callable.If a curried functionf0is called with too many arguments,
and if its result is a functionf1,
thenf1is automatically called with the arguments thatf0did not use.From a mathematical point of view, it is not currying but uncurrying:a -> (b -> c)  ===uncurry==>  (a, b) -> cIndeed, by accepting more arguments than necessary,yummycurryeffectively
turns a function-returning-function (a->(b->c))
into a function of several parameters ((a, b)->c).The process repeats itself automatically until it runs out of arguments or the
result is not callable:def one_param_only(x):
    def i_eat_leftovers(y):
        return x + y
    return i_eat_leftovers

try:
    greeting = one_param_only('hello ', 'world')
except TypeError:
    pass  # We knew it would not work.Withyummycurryyou can call a one-parameter function with more than one
argument.
In our example,one_param_onlydoes not use'world',
socurrypasses it to the result ofone_param_only, which is
ai_eat_leftoversclosure:greet = curry(one_param_only)
greeting = greet('hello ', 'world')
assert greeting == 'hello world'Until now, we have always calledcurryor@currywith a single argument:
the callable to curry.
However, it is possible to give more arguments tocurry; they will simply
be passed to the underlying callable.The three following snippets are equivalent:greet = curry(one_param_only)
greeting = greet('hello ', 'world')
assert greeting == 'hello world'

greet = curry(one_param_only, 'hello ')
greeting = greet('world')
assert greeting == 'hello world'

greeting = curry(one_param_only, 'hello ', 'world')
assert greeting == 'hello world'It is an error to have left-over arguments when the automatic application stops:# Good:
assert curry(inc, 123) == 124

# Bad:
try:
    curry(inc, 123, 456, x=789)
except TypeError:
    passIt raisesTypeError:left-overarguments at the end of evaluation:*(456,),**{'x':789}.In that example,inc(123)returns the integer124which is not callable
and does not know what to do with the extra arguments.
Instead of letting Python return its typicalTypeError: 'int' object is not callable,yummycurrygives an error message that lists the leftover parameters, which
helps with debugging.Keyword argumentsIn addition to positional parameters, Python also has keyword parameters.One can useyummycurryand keyword arguments when the order of the
positional parameters is inconvenient
(except for positional-only parameters in Python >=3.8 which will never
accept being fed by a keyword argument):@curry
def list_map(f, iterable):
    return list(map(f, iterable))

primes = [2, 3, 5, 7]

over_primes = list_map(iterable=primes)

assert over_primes(inc) == [3, 4, 6, 8]Conflicts between keyword and positional argumentsKeyword arguments and positional arguments can fight over names.
Thecurryfunction is designed to break whenever Python would break (with
error messages close to the original ones).For example, if a positional-only parameter (Python >=3.8) is fed by
a keyword argument, bothcurryand undecorated functions
raiseTypeError.If a positional-or-keyword parameter is fed both by a positional and
a keyword argument,TypeErroris raised.@curry
def give_name(who, name, verbose=False):
    if verbose:
        print('Hello', name)
    new_who = {**who, 'name':name}
    return new_who

@curry
def create_genius(iq: int, best_quality:str, *, verbose=False):
    you = dict(iq = 50, awesome_at=best_quality)
    if iq > you['iq']:
        you['iq'] = iq
        if verbose:
            print('Boosting your iq to', iq)
    else:
        if verbose:
            print('You are already smart enough')
    return give_name(you)Consider the following call:dear_reader = create_genius('spitting fire', name='Darling', iq=160, verbose=True)That call raisesTypeError: multiple values for argument 'iq', as it would
if it were not decorated with@curry.
It would have been possible to makecurrydetect
thatiqis passed as a keyword,
and conclude that'spitting fire'should go tobest_quality,
but this would make the decorated and undecorated versions behave differently.
Indeed, Python complains in this situation for the undecorated function.
In order to be transparent and predictable,currycomplains as well.One could think that doing it in two steps would resolve the ambiguity:smart = create_genius(name='Darling', iq=160, verbose=True)
dear_reader = smart('spitting fire')but it does not, which is a good thing.
In this case, the signature ofsmartis(best_quality: str),
and we properly call it with a string.
Nevertheless it still raises the sameTypeErroraboutiqhaving more
than one value.
This is by design.
The order of the keyword arguments, and the number of calls that sets them,
should not matter.  If it breaks in one case, it should breaks in all cases.
Otherwise that is a debugging nightmare.Two exceptions to this rule: variable-argument parameters
(*argsand**kwargs), and parameters with default values.
As shown later in this document, those break the symmetry.There are many ways to fix this call.
For example, if we insist in passingnameandiqas keywords, then
it is necessary to passbest_qualityas a keyword as well to remove all
ambiguity.
This can be done in any order, in as many calls as wanted:dear_reader = create_genius(
    best_quality='spitting fire',
    name='Darling',
    iq=160,
    verbose=True
)

# ... equivalent to ...

smart = create_genius(name='Darling', iq=160, verbose=True)
dear_reader = smart(best_quality='spitting fire')Keyword arguments are used only onceIf you run the code above, you will notice that settingverbose=Truemakescreate_geniusprint something.
However,give_namedoes not print anything.
This happens becausecurryuses arguments only once.
Whencreate_geniusreturns thegive_namefunction, theverboseargument has already been consumed.Variable positional and keyword argumentsIf a callable has a variable-argument parameter, whether positional or keyword,
then it will take all the available arguments and will not pass them down the
call chain:@curry
def greedy(x, *args):
    if args:
        print('I am stealing your', args)
    def starving(y):
        return x + y
    return starving

assert greedy(10)(1) == 11Here,greedyis satisfied with one argument (even if it could take more)
so it executes and returns thestarvingclosure which takes1.
Because of this, we break the general rule-of-thumb thatf(x)(y)== f(x, y).
Indeed:try:
    assert greedy(10, 1) == 11
except AssertionError:
    passHere,greedytakes the1it its*args, it even brags about it
with its print statement.  Then, satisfied, it executes.  The result
is thestarvingclosure.  That closure does not receive any argument
to feed its parameter so it cannot execute, it remains callable, it is
not an integer and therefore is not equal to 11.
There is no workaround, one must givestarvingits own argument:assert greedy(10, 1000, 2000, 3000, 4000)(1) == 11The same rule applies for variable-keyword-argument parameters:@curry
def black_hole(mass, **slurp):
    def hawking_radiation(*, bleep):
        return 'tiny {}'.format(bleep)

    return hawking_radiation

assert black_hole(10, bleep='proton', curvature='thicc')(bleep='neutrino') == 'tiny neutrino'Here, the black hole swallowed our bleeping proton,
so the Hawking radiation requires that we specify a new bleep.Be careful: currying a function that takes only variable arguments will execute
it immediately since its signature is satisfied by getting nothing at all.As mentioned earlier in this document, variable-argument parameters break the
general rule of thumb thatf(x)(y)== f(x, y).Inspection and debuggingstrCurried functions are easy on the eyes when given tostr.
This is achieved by using the__name__attribute of underlying callables,
if they have one:@curry
def inc(x: int) -> int:
    return x + 1

@curry
def dbl(x: int) -> int:
    return x * 2

def _compose(f: Callable[[int], int], g: Callable[[int], int], x: int) -> int:
    return f(g(x))

compose = curry(_compose)  # __name__ will retain the underscore.

assert str(compose(inc, dbl)) == '_compose(inc, dbl)'  # Note the underscore.
assert str(compose(inc, x=10)) == '_compose(inc, x=10)'CurriedclassUsing__repr__reveals that the composed function is in fact
an object of typeCurried:print(repr(compose(inc, x=10))
# Curried(<function _compose at 0x000001F8D864A550>,
# (Curried(<function inc at 0x000001F8D864A430>, (), {},
# <Signature (x: int) -> int>),), {'x': 10},
# <Signature (g: Callable[[int], int]) -> int>)ThatCurriedobject can be deconstructed with the attributesfunc,argsandkeywords(same attribute names asfunctool.partialobjects):i10 = compose(inc, x=10)
assert i10.func == _compose
assert i10.args == (inc,)
assert i10.keywords == dict(x=10)TheCurriedobject also updates its signature to reflect the parameters
that its callable still needs.
In our example, the callablei10(ourCurriedobject), still expects a
parametergwhich is a function frominttoint.
The signature can be accessed via the__signature__attribute, which is
of typeinspect.Signature:import inspect

assert i10.__signature__ == inspect.signature(i10)
print(i10.__signature__)  # (g: Callable[[int], int]) -> intNote that static type checking tools likeMyPyare unlikely to understand this,
as they look at the code but do not execute it.Parameters with default valuesUnder the hood,currycompares the result ofinspect.signatureto the
positional and keyword arguments collected so far.
As soon as the function can be called, it is called.
This means thatcurrydoes not wait when a parameter has a default value:@curry
def increase(x:int, increment:int=1):
    return x + increment

assert increase(10) == 11  # Does not wait for ``increment``.

assert increase(10, increment=100) == 110

inc_100 = increase(increment=100)
assert inc_100(10) == 110Parameters with default values break the general rule-of-thumb thatf(x, y) ==f(x)(y).Currying classes, class methods and instance methodsInstance and class methods can also be curried:class Rabbit:
    def __init__(self, ears, tails):
        self._ears = ears
        self._tails = tails

    @curry_method  # Works here like a read-only property
    def ears(self):
        return self._ears

    @curry_method
    def tails(self):
        return self._tails

    @curry_classmethod
    @classmethod
    def breed(cls, rabbit1, rabbit2):
        # Accurate model of rabbit genetics.
        return cls(
            (rabbit1.ears + rabbit2.ears) / 2,  # Yes, floats.
            rabbit1.tails * rabbit2.tails,
        )

    @curry_method
    def jump(self, impulse, target):
        # Does not mean anything, just a demonstration.
        return [impulse, target, 'boing']

thumper = Rabbit(2, 1)
monster = Rabbit(3, 2)

thumperize = Rabbit.breed(thumper)
oh_god_no = thumperize(monster)  # Currying a class method.
assert oh_god_no.ears == 2.5
assert oh_god_no.tails == 2

thumper_jump = thumper.jump('slow')
assert thumper_jump('west') == ['slow', 'west', 'boing']And of course, you can curry the class itself:rabbit = curry(Rabbit)
deaf = rabbit(ears=0)
beethoven = deaf(tails=10)  # 5 per hand.
assert beethoven.ears == 0
assert beethoven.tails == 10