0

We start off with a function, such as the following:

def funky_function(x1, x2, x3, x4, /):
    return ", ".join(" ".join(str(x).split()) for x in [x1, x2, x3, x4])

After decorating the function, we should have to write parentheses around each and every calling argument.

r = f(1)(2)(3)(4) # GOOD
# r = f(1, 2, 3, 4) # BAD

One potential application is generalizing a decorator.
Some decorators only work on single-argument functions.

We might want a new decorator which works on multi-argument functions.

For example, you could implement function overloading (multiple-dispatching) by generalizing functools.singledispatch. Other people have already implemented multi-methods; so that is not really what my question is about. However, I wanted to provide some an example application as motivation.

from functools import singledispatch

    @singledispatch
    def fun(arg):
        pass  

    @fun.register
    def _(arg: int):
        print("I am processing an integer")
   
    @fun.register
    def _(arg: list):
        print("I am processing a list")

I attempted to write some code to accomplish this task, but it does not exhibit the desired behavior. Ideally, a decorated function becomes a function of one argument which returns another function.

return_value = f(1)(2)(3)(4)  

Here is some code:

from functools import *
from inspect import *

class SeperatorHelper:
    def __init__(self, func):
        """`func` should be callable"""
        assert(callable(func))
        self._func = func
    def __call__(self, arg):
        return type(self)(partial(self._func, arg))

def seperate_args(old_func, /):
    """
        This is a decorator

        @seperate_args
        def foo(x1, x2, x3, x4, /):
            pass

       +------------------+------------------+
       |       NEW        |       OLD        |
       +------------------+------------------+
       | f(1)(2)(3)(4)    | f(1, 2, 3, 4)    |
       +------------------+------------------+
    """
    new_func = SeperatorHelper(old_func)
    new_func = wraps(old_func)(new_func)
    return new_func

#######################################################
# BEGIN TESTING
#######################################################

@seperate_args
def funky_function(x1, x2, x3, x4, /):
    return ", ".join(" ".join(str(x).split()) for x in [x1, x2, x3, x4])

print("signature == ", signature(funky_function))

func_calls = [
    "funky_function(1)(2)",
    "funky_function(1)(2)(3)(4)",
    "funky_function(1)(2)(3)(4)("extra arg")",
    "funky_function(1)(2)(3)(4)()()()()"
]

for func_call in func_calls:
    try:
        ret_val = eval(func_call)
    except BaseException as exc:
        ret_val = exc

    # convert `ret_val` into a string 
    # and eliminate line-feeds, tabs, carriage-returns...
    ret_val = " ".join(str(ret_val).split())

    print(40*"-")
    print(func_call)
    print("return value".ljust(40), )
    print(40 * "-")
Toothpick Anemone
  • 4,290
  • 2
  • 20
  • 42
  • 1
    Heh. Have you heard of functions that do "currying"? That's what you're writing here; in some languages it's the standard way _all_ functions work. What's the point of messing with `eval`? – Charles Duffy Sep 18 '22 at 00:15
  • Since you only need to support the simple case where a function has a fixed number of arguments, there's no need for runtime testing; you can just inspect the function to get its argument count, and recursively construct a function with the appropriate level of nesting. – Charles Duffy Sep 18 '22 at 00:18
  • anyhow -- knowing the name "currying" should do you a lot of good towards solving the problem on your own. f/e, it allows a Google search which comes up with plenty of results -- f/e https://www.geeksforgeeks.org/currying-function-in-python/; it doesn't quite give you a decorator for free, but it tells you what your decorator should be doing. – Charles Duffy Sep 18 '22 at 00:19
  • Also, a very useful tool for currying is [`functools.partial`](https://docs.python.org/3/library/functools.html#functools.partial); using it will make your job easier. :) – Charles Duffy Sep 18 '22 at 00:20
  • (for more background -- which I really do recommend reading to better understand your assignment -- see [What is the difference between currying and partial application?](https://stackoverflow.com/questions/218025/what-is-the-difference-between-currying-and-partial-application)) – Charles Duffy Sep 18 '22 at 00:22
  • @CharlesDuffy learning the word "currying" will help immensely. I did not know that is what other programmers called this. – Toothpick Anemone Sep 19 '22 at 03:45

1 Answers1

0

As a quick attempt that doesn't attempt to handle corner cases comprehensively --

from inspect import signature
from functools import partial

def curry(orig_func):
    sig = signature(orig_func)
    target_arg_count = len(sig.parameters)
    args=[]
    new_func = orig_func
    for n in range(len(sig.parameters)):
        def curry_step(*args):
            if len(args) < target_arg_count:
                return partial(new_func, *args)
            return orig_func(*args)
        new_func = curry_step
    return new_func

@curry
def funky_function(x1, x2, x3, x4, /):
    return ", ".join(" ".join(str(x).split()) for x in [x1, x2, x3, x4])
Charles Duffy
  • 280,126
  • 43
  • 390
  • 441