-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
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: type safe function composition #8449
Comments
I'm not quite sure about the exact thing you are trying to do. Would you like to a function |
Hi @JukkaL, yes that’s exactly what I’m trying to do. I tried writing an extension and I came to the same conclusion. It’s too complicated for mypy to understand. I was hoping the mypy devs maybe knew something I didn’t :) |
Is it possible to statically type a generic compose function that only takes two arguments? E.g. A = TypeVar('A')
X = TypeVar('X')
Y = TypeVar('Y')
Z = TypeVar('Z')
def compose(f: Callable[[X], Y], g: Callable[[Y], Z]) -> Callable[[X], Z]:
def _compose(x: X) -> Z:
return g(f(x))
return _compose
def f(a: A) -> A:
return a
def g(a: int) -> int:
return a
compose(f, g)(3) # error: Cannot infer type of argument 2 of "compose" |
It is possible, I think you mixed up some types in your A = TypeVar("A")
B = TypeVar("B")
X = TypeVar("X")
Y = TypeVar("Y")
Z = TypeVar("Z")
def id(x: A) -> A:
return x
def compose(f: Callable[[Y], Z], g: Callable[[X], Y]) -> Callable[[X], Z]:
def _compose(x: X) -> Z:
return f(g(x))
return _compose
def to_str(x: B) -> str:
return str(x)
res: str = id("foo")
foo: str = to_str(id(4))
my_fn: Callable[[B], str] = compose(id, to_str)
my_fn2: Callable[[A], str] = compose(to_str, id)
my_res: str = my_fn(4) |
I expected the following to work (composition of functions from typing import Callable, TypeVar
T = TypeVar('T')
def compose(*functions: Callable[[T], T]) -> Callable[[T], T]:
def composition(x):
y = x
for f in functions:
y = f(y)
return y
return composition
def identity(x: T) -> T:
return x
f = compose(identity, identity)
reveal_type(f) # Revealed type is "def (T`-1) -> T`-1"
f(1) # error: Argument 1 has incompatible type "int"; expected "T" Is this just a limitation or a bug? EDIT: note that using a different from typing import Callable, TypeVar
A = TypeVar('A')
B = TypeVar('B')
def compose(*functions: Callable[[A], A]) -> Callable[[B], B]:
def composition(x):
y = x
for f in functions:
y = f(y)
return y
return composition
def identity(x: A) -> A:
return x
f = compose(identity, identity)
reveal_type(f) # Revealed type is "def (T`-1) -> T`-1"
res = f(1)
reveal_type(res) # Revealed type is "builtins.int*" |
…ed_params Removed aliases of Callable[[C], C] as OptionAdder etc... they added no value and I misused them: I thought they worked as if I wrote Callable[[C], C] but that's not the case. Once I removed those aliases and used generics properly, I run into a MyPy issue/limitation (not sure) with generic "function composition", which is what @option_group and @constrained_params do: python/mypy#8449 (comment). I worked around it by using different type variables for arguments and return type (F and G). Besides the annoyance, type inference seems to work well.
Relevant discussion regarding compose as an operator (or maybe a classmethod) and variadic functions is also being discussed on pytoolz/toolz#523. Would love to hear more opinions. |
Couple notes that it would be nice to keep in mind when implementing type-checking support for function composition:
|
This example type checks with modern mypy: #8449 (comment) |
@hauntsaninja this issue is about
The example you linked to only works for a That technique can be adjusted to other hard-coded amounts of functions, and can be scaled with
|
Also, type checking for Consider this type hint for my P = ParamSpec('P')
R1 = TypeVar('R1')
R2 = TypeVar('R2')
def acompose(f2: Callable[[R1], Union[R2, Awaitable[R2]]], f1: Callable[P, Union[R1, Awaitable[R1]]], /) -> Callable[P, Awaitable[R2]]:
... As of this morning, a fresh install of MyPy on Python 3.11 errors out (something about expecting a return type of |
I wonder though, is this possible to express without changes to introduce some new type level construct? If not, perhaps the issue should be closed anyway, with encouragement to propose adding such a construct with a PEP. (I think this conceptually needs a type-level for loop, to be able to express each function is compatible with the next) |
@mentalisttraceur I would encourage opening a separate bug report for the issue you are seeing with async functions. |
@antonagestam I agree that this is best solved by a new typing construct through a PEP. I probably don't have the time and energy to pursue a PEP right now, but I'll jot down an idea for now: Idea
ExampleThe type hint for P = ParamSpec('P')
R = TypeVar('R')
R1 = TypeVar('R1')
R2 = TypeVar('R2')
@override
def compose(*functions: RelatedTypes[Callable[[Any], R], Relation[Callable[[R1], R2], Callable[..., R1]], Callable[P, R1]) -> Callable[P, R]:
... DetailsI would love to hear thoughts from implementers of type checkers on this! Did I miss some reason why this is infeasible/horrible/inefficient to implement?
In other words, given a signature like
|
@mentalisttraceur I think the post you just made here would be great to take to the typing forum, you can likely get some feedback from the community and hopefully also type checker maintainers there. |
@antonagestam Cheers, done. |
There are many third party libraries, which aim to provide functional programming tools. Examples of this are fn.py and and funcy. They both provide a
compose
function that allows you to chain together an arbitrary number of functions. However none of the implementations are compatible with MyPy. Has there been any thought on the MyPy team about supporting such a feature, possibly in the typing submodule? I was able to get such a feature working with composing two functions, similar to this blog post https://www.fabianism.us/blog/2015/11/18/function-composition-with-types.html. But this doesn't scale to an arbitrary number of functions.The text was updated successfully, but these errors were encountered: