Search code examples
pythonpartialcurrying

Python currying and partial


While doing programming exercises on codewars.com, I encountered an exercise on currying and partial functions.

Being a novice in programming and new to the topic, I searched on the internet for information on the topic and got quite far into solving the exercise. However I have now stumbled upon an obstacle I can't seem to overcome and am here looking for a nudge in the right direction.

The exercise is rather simple: write a function that can curry and/or partial any input function and evaluates the input function once enough input parameters are supplied. The input function can accept any number of input parameters. Also the curry/partial function should be very flexible in how it is called, being able to handle many, many different ways of calling the function. Also, the curry/partial function is allowed to be called with more inputs than required by the input function, in that case all the excess inputs need to be ignored.

Following the exercise link, all the test cases can be found that the function needs to be able to handle.

The code I came up with is the following:

from functools import partial
from inspect import signature

def curry_partial(func, *initial_args):
    """ Generates a 'curried' version of a function. """

    # Process any initial arguments that where given. If the number of arguments that are given exceeds 
    # minArgs (the number of input arguments that func needs), func is evaluated

    minArgs = len(signature(func).parameters)
    if initial_args:
        if len(initial_args) >= minArgs: 
            return func(*initial_args[:minArgs])

        func = partial(func, *initial_args)
        minArgs = len(signature(func).parameters)

    
    # Do the currying
    def g(*myArgs):
        nonlocal minArgs

        # Evaluate function if we have the necessary amount of input arguments
        if minArgs is not None and minArgs <= len(myArgs):
                return func(*myArgs[:minArgs]) 
            
        def f(*args):
            nonlocal minArgs
            newArgs = myArgs + args if args else myArgs

            if minArgs is not None and minArgs <= len(newArgs):
                return func(*newArgs[:minArgs])
            else:
                return g(*newArgs)  
        return f
    return g

Now this code fails when the following test is executed:

test.assert_equals(curry_partial(curry_partial(curry_partial(add, a), b), c), sum)

where add = a + b + c (properly defined function), a = 1, b = 2, c = 3, and sum = 6.

The reason this fails is because curry_partial(add, a) returns a function handle to the function g. In the second call, curry_partial(<function_handle to g>, b), the calculation minArgs = len(signature(func).parameters) doesn't work like I want it to, because it will now calculate how many input arguments function g requires (which is 1: i.e. *myArgs), and not how many the original func still requires. So the question is, how can I write my code such that I can keep track of how many input arguments my original func still needs (reducing that number each time I am partialling the function with any given initial arguments).

I still have much to learn about programming and currying/partial, so most likely I have not chosen the most convenient approach. But I'd like to learn. The difficulty in this exercise for me is the combination of partial and curry, i.e. doing a curry loop while partialling any initial arguments that are encountered.


Solution

  • Try this out.

    from inspect import signature
    
    # Here `is_set` acts like a flip-flop
    is_set = False
    params = 0
    
    def curry_partial(func, *partial_args):
        """
        Required argument: func
        Optional argument: partial_args
        Return:
            1) Result of the `func` if
               `partial_args` contains
               required number of items.
            2) Function `wrapper` if `partial_args`
               contains less than the required
               number of items.
        """
    
        global is_set, params
        
        if not is_set:
            is_set = True
            
            # if func is already a value
            # we should return it
            try: params = len(signature(func).parameters)
            except: return func
        
        try:
            is_set = False
            return func(*partial_args[:params])
        
        except:
            is_set = True
        
            def wrapper(*extra_args):
                """
                Optional argument: extra_args
                Return:
                    1) Result of the `func` if `args`
                       contains required number of
                       items.
                    2) Result of `curry_partial` if
                       `args` contains less than the
                       required number of items.
                """
                
                args = (partial_args + extra_args)
                
                try:
                    is_set = False
                    return func(*args[:params])
                except:
                    is_set = True
                    return curry_partial(func, *args)
        
        return wrapper
    

    This indeed isn't very good by design. Instead you should use class, to do all the internal works like, for example, the flip-flop (don't worry we don't need any flip-flop there ;-)).

    Whenever there's a function that takes arbitrary arguments, you can always instantiate that class passing the function. But this time however, I leave that on you.