Python currying e parcial
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
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ê.
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.