morkel
morkel

Reputation: 544

apply transformation on a ParamSpec variable?

Is there any way for me to apply a transformation on a ParamSpec? I can illustrate the problem with an example:

from typing import Callable

def as_upper(x: str):
    return x.upper()

def eventually(f: Callable[P, None], *args: P.args, **kwargs: P.kwargs):
    def inner():
        def transform(a):
            return a() if isinstance(a, Callable) else a
        targs = tuple(transform(a) for a in args)
        tkwargs = {k: transform(v) for k,v in kwargs.items()}
        return f(*targs, **tkwargs)
    return inner

eventually(as_upper, lambda: "hello")  # type checker complains here

This type checker (pyright in my case) will complain about this. The function eventually received a callable () -> str and not a str which was expected. My question is: is there some way for me to specify that it should expect () -> str and not the str itself? And in general, if a function expects a type T I can transform it to (say) () -> T?

I'm basically asking if it's possible to transform the ParamSpec in some way so that a related function does not expect the same parameters, but "almost" the same parameters.

I don't really expect this to be possible, but maybe some with more experience with type checking know a potential solution to this problem. :)

Upvotes: 4

Views: 1969

Answers (1)

PIG208
PIG208

Reputation: 2380

This decorator cannot be properly typed with currently available tools (Python 3.10).

Two main problems here:

  • ParamSpec and Concatenate for now only allow us to modify a fixed number of parameters.
  • We cannot concatenate keyword-only arguments (which makes transforming **kwargs: P.kwargs impossible)

However, under these constraints, we can achieve a less elegant solution if the positional parameters are known, taking advantage of currying:

from typing import Callable, Concatenate, ParamSpec, TypeVar

T = TypeVar("T")
RetT = TypeVar("RetT")
P = ParamSpec("P")

def something(a: str, b: int, c: bool):
    return f"{a} {b} {c}"

def eventually(
    f: Callable[Concatenate[T, P], RetT], a: Callable[[], T]
) -> Callable[P, RetT]:
    def inner(*args: P.args, **kwargs: P.kwargs) -> RetT:
        transformed = a() if callable(a) else a
        return f(transformed, *args, **kwargs)

    return inner


something_eventually = eventually(
    eventually(eventually(something, lambda: "hello"), lambda: 2), lambda: False
)
something_eventually()  # hello 2 False

Notice that it is yet not possible to concatenate keyword parameters (See also).

eventually can also be applied in a more functional way (but it cannot be properly typed):

from functools import reduce

# Though it unfortunately doesn't type check
something_eventually = reduce(
    eventually,
    [lambda: "hello", lambda: 2, lambda: False],
    something,
)
something_eventually()  # hello 2 False

reduce expects the type of the value to have the same type, while we are changing the type of the function in each iteration. This makes it impossible to type it when we apply eventually arbitrary times in this manner.

reduce is just an example. In a more general sense, I doubt if we can properly type something that involves repeatedly applying currying functions like eventually as proposed above, at least with the current Concatenate support.

Upvotes: 5

Related Questions