dry-python / dry-python/returns

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

Open
#1,692 1 comment 3 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
4.4k
Forks
155
Avg merge
2h 27m
Merged PRs (30d)
22

Description

# Feature Request/Enhancement

I stumbled upon this post a while ago:

https://github.com/python/typing/discussions/1245?sort=top#discussioncomment-3454192

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:
```python
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:

```python
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
raise e
File "/home/pratik/workplace/dry-python-returns/problem.py", line 16, in
print(fizz(1, 2))
^^^^^^^^^^
TypeError: pipe..() 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.

```python
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:

```python
# 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: https://github.com/python/typing/discussions/1289

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.