Python currying e parcial

Aug 19 2020

Ao fazer exercícios de programação em codewars.com, encontrei um exercício sobre currying e funções parciais.

Sendo um novato em programação e novo no assunto, pesquisei na internet informações sobre o assunto e fui muito longe na resolução do exercício. No entanto, agora me deparei com um obstáculo que não consigo superar e estou aqui procurando um empurrãozinho na direção certa.

O exercício é bastante simples: escreva uma função que pode curry e / ou parcial qualquer função de entrada e avalia a função de entrada uma vez que parâmetros de entrada suficientes são fornecidos. A função de entrada pode aceitar qualquer número de parâmetros de entrada. Além disso, a função curry / parcial deve ser muito flexível na forma como é chamada, sendo capaz de lidar com muitas, muitas maneiras diferentes de chamar a função. Além disso, a função curry / parcial pode ser chamada com mais entradas do que o exigido pela função de entrada; nesse caso, todas as entradas em excesso precisam ser ignoradas.

Seguindo o link do exercício, todos os casos de teste podem ser encontrados que a função precisa ser capaz de lidar.

O código que criei é o seguinte:

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

Agora, esse código falha quando o seguinte teste é executado:

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

onde add = a + b + c (função definida corretamente), a = 1, b = 2, c = 3 e sum = 6.

O motivo da falha é porque curry_partial(add, a)retorna um identificador de função para a função g. Na segunda chamada curry_partial(<function_handle to g>, b),, o cálculo minArgs = len(signature(func).parameters)não funciona como eu desejo, porque agora calculará quantos argumentos de entrada a função grequer (que é 1: ou seja *myArgs,), e não quantos o original funcainda requer. Portanto, a questão é: como posso escrever meu código de forma a manter o controle de quantos argumentos de entrada meu original funcainda precisa (reduzindo esse número cada vez que participo da função com quaisquer argumentos iniciais fornecidos).

Ainda tenho muito que aprender sobre programação e currying / parcial, então provavelmente não escolhi a abordagem mais conveniente. Mas gostaria de aprender. A dificuldade neste exercício para mim é a combinação de parcial e curry, ou seja, fazer um loop de curry enquanto partialling quaisquer argumentos iniciais que sejam encontrados.

Respostas

1 Ava Aug 20 2020 at 13:27

Experimente isso.

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

Isso realmente não é muito bom por design. Em vez disso, você deve usar class, para fazer todos os trabalhos internos como, por exemplo, o flip-flop (não se preocupe, não precisamos de nenhum flip-flop lá ;-)).

Sempre que houver uma função que receba argumentos arbitrários, você poderá instanciar essa classe passando a função. Mas desta vez, deixo isso com você.

Nishant Aug 21 2020 at 02:46

Não tenho certeza sobre currying , mas se você precisar de um gerador de função parcial simples, pode tentar algo assim:

from functools import partial
from inspect import signature

def execute_or_partial(f, *args):
    max = len(signature(f).parameters)
    if len(args) >= max: 
        return f(*args[:max])
    else:
        return partial(f, *args)

s = lambda x, y, z: x + y + z

t = execute_or_partial(s, 1)
u = execute_or_partial(t, 2)
v = execute_or_partial(u, 3)

print(v)

or

print(execute_or_partial(execute_or_partial(execute_or_partial(s, 1), 2), 3))

Mesmo que não resolva seu problema original, veja se você pode usar o código acima para reduzir a repetição de código (não tenho certeza, mas acho que há alguma repetição de código na função interna?); isso tornará os problemas subsequentes mais fáceis de resolver.

Pode haver funções na biblioteca padrão que já resolvem esse problema. Muitas linguagens puramente funcionais, como Haskell, possuem esse recurso embutido na linguagem.