Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature Request: Let Pipe function take more than 1 positional argument. #1692

Open
PratikBhusal opened this issue Sep 20, 2023 · 1 comment

Comments

@PratikBhusal
Copy link

PratikBhusal commented Sep 20, 2023

Feature Request/Enhancement

I stumbled upon this post a while ago:

python/typing#1245

It was pretty neat, and wanted to try it out for functions that take more than 1 positional argument. Sadly, it does not seem to work.

What's wrong

When trying to run the following code:

from returns._internal.pipeline.pipe import pipe


def one_arg_only(num1: float) -> int:
    return int(num1)


def two_args(num1: float, num2: int) -> int:
    return int(num1 + num2)


def to_string(f: float) -> str:
    return str(f)


def to_float(s: str) -> float:
    return float(s)


if __name__ == "__main__":
    fizz = pipe(one_arg_only, to_string, to_float)
    print(fizz(1))  # Trivial example

    buzz = pipe(to_string, to_float)
    print(buzz(two_args(1, 2)))  # This works, but is not ideal

    bazz = pipe(two_args, to_string, to_float)
    try:
        print(bazz(1, 2))  # Too many arguments for "__call__" of "_Pipe" [call-arg]
    except TypeError as e:
        print(
            "Cannot pass two arguments even though first function requires 2 positional arguments"
        )
        raise e

you would get the following exception:

Cannot pass two arguments even though first function requires 2 positional arguments
Traceback (most recent call last):
  File "/home/pratik/workplace/dry-python-returns/problem.py", line 21, in <module>
    raise e
  File "/home/pratik/workplace/dry-python-returns/problem.py", line 16, in <module>
    print(fizz(1, 2))
          ^^^^^^^^^^
TypeError: pipe.<locals>.<lambda>() takes 1 positional argument but 2 were given

How is that should be

Invoking bazz should not throw a TypeError.

What I have tried

The following works if and only if the types are all correct.

from functools import reduce
from typing import overload, ParamSpec, TypeVar, Callable


_P = ParamSpec("_P")
_T1 = TypeVar("_T1")
_T2 = TypeVar("_T2")
_T3 = TypeVar("_T3")

@overload
def pipe(f1: Callable[_P, _T1]) -> Callable[_P, _T1]: ...
@overload
def pipe(
    f1: Callable[_P, _T1],
    f2: Callable[[_T1], _T2],
) -> Callable[_P, _T2]: ...
@overload
def pipe(
    f1: Callable[_P, _T1],
    f2: Callable[[_T1], _T2],
    f3: Callable[[_T2], _T3],
) -> Callable[_P, _T3]: ...

def pipe(*functions):
    def compose2(f, g):
        return lambda *args, **kwargs: g(f(*args, **kwargs))

    return reduce(compose2, functions)

When the types are not correct, mypy throws the following error message:

# Mypy error says: Cannot infer type argument 3 of "pipe"
fizz = pipe(one_arg_only, to_string, to_string)

I toyed around with typing.ParamSpec within returns/_internal/pipeline/pipe.pyi to see if that would do the trick, but I couldn't after a couple of hours of getting the above example to have mypy return no errors. Maybe I missed something, or I just have mind block.

Related issue: python/typing#1289

@sobolevn
Copy link
Member

Any PR is welcome if you have a working prototype.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

2 participants