Discussion on Compose type operator

Hi, Context: during the typing meeting of this week Brandon presented the idea of introducing a Compose type operator, which takes a variadic of callables, checks that they compose, and returns the type of the composed function. To follow-up on the discussion about Compose: We were wondering what can be achieved nowadays without custom operators, just relying on overload. Let's think about the example of a compose function, we could do the following right? from typing import Callable as Call # Overloads def compose(f1: Call[A,B]) -> Call[A,B] def compose(f1: Call[A,B], f2: Call[B,C]) -> Call[A,C] def compose(f1: Call[A,B], f2: Call[B,C], f3: Call[C,D]) -> Call[A,D] ... What is less obvious is how would we type a class like torch.nn.Sequential, which would look like this with the Compose operator: class Sequential(Generic[A,*Ts]): def __init__(self, fs: *Ts) def __call__(self, in: A) -> Compose[*Ts] Here we can't overload the class definition, so what can be done? We had a couple of ideas but they would not really work for a variable number of arguments because we can't overload. An example of those approaches would be: class Sequential(Generic[A,B,C]): def __init__(self, f1: Callable[A,B], f2: Callable[B,C]): ... def __call__(self, in: A) -> C: .... Best, Alfonso.

I have an example implementation of this concept. Source: https://github.com/dry-python/returns/blob/master/returns/_internal/pipeline... Plugin: https://github.com/dry-python/returns/blob/master/returns/contrib/mypy/_feat... Docs: https://returns.readthedocs.io/en/latest/pages/pipeline.html Tests: https://github.com/dry-python/returns/tree/master/typesafety/test_pipeline/t... But, it still has some inference problems when used with some complex generics / lambdas. Best, Nikita Sobolev https://github.com/sobolevn/ пн, 16 авг. 2021 г. в 21:13, Alfonso L. Castaño <alfonsoluis.castanom@um.es
:
Hi,
Context: during the typing meeting of this week Brandon presented the idea of introducing a Compose type operator, which takes a variadic of callables, checks that they compose, and returns the type of the composed function.
To follow-up on the discussion about Compose:
We were wondering what can be achieved nowadays without custom operators, just relying on overload. Let's think about the example of a compose function, we could do the following right?
from typing import Callable as Call
# Overloads def compose(f1: Call[A,B]) -> Call[A,B] def compose(f1: Call[A,B], f2: Call[B,C]) -> Call[A,C] def compose(f1: Call[A,B], f2: Call[B,C], f3: Call[C,D]) -> Call[A,D] ...
What is less obvious is how would we type a class like torch.nn.Sequential, which would look like this with the Compose operator:
class Sequential(Generic[A,*Ts]): def __init__(self, fs: *Ts) def __call__(self, in: A) -> Compose[*Ts]
Here we can't overload the class definition, so what can be done? We had a couple of ideas but they would not really work for a variable number of arguments because we can't overload. An example of those approaches would be:
class Sequential(Generic[A,B,C]): def __init__(self, f1: Callable[A,B], f2: Callable[B,C]): ... def __call__(self, in: A) -> C: ....
Best, Alfonso. _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: n.a.sobolev@gmail.com

It'd be nice if Sequential had the same type parameter structure as Linear, which we've been assuming will be generic in just the input and output types: class Linear(Generic[In, Out]): def __call__(x: Tensor[Batch, In]) -> Tensor[Batch, Out]: ... That would suggest: class Sequential(Generic[A, C]): def __init__(self, f1: Callable[[A], B], f2: Callable[[B], C]): pass I think the reason we thought this wouldn't work when discussing it in the tensor typing meeting was that it would rely on a type variable B that wasn't one of the type parameters to the class - but surprisingly, it seems to be fine in mypy: https://mypy-play.net/?mypy=latest&python=3.10&gist=e4b1d2d7b70d79f62eadeca3d711193d On Mon, 16 Aug 2021 at 19:13, Alfonso L. Castaño <alfonsoluis.castanom@um.es> wrote:
Hi,
Context: during the typing meeting of this week Brandon presented the idea of introducing a Compose type operator, which takes a variadic of callables, checks that they compose, and returns the type of the composed function.
To follow-up on the discussion about Compose:
We were wondering what can be achieved nowadays without custom operators, just relying on overload. Let's think about the example of a compose function, we could do the following right?
from typing import Callable as Call
# Overloads def compose(f1: Call[A,B]) -> Call[A,B] def compose(f1: Call[A,B], f2: Call[B,C]) -> Call[A,C] def compose(f1: Call[A,B], f2: Call[B,C], f3: Call[C,D]) -> Call[A,D] ...
What is less obvious is how would we type a class like torch.nn.Sequential, which would look like this with the Compose operator:
class Sequential(Generic[A,*Ts]): def __init__(self, fs: *Ts) def __call__(self, in: A) -> Compose[*Ts]
Here we can't overload the class definition, so what can be done? We had a couple of ideas but they would not really work for a variable number of arguments because we can't overload. An example of those approaches would be:
class Sequential(Generic[A,B,C]): def __init__(self, f1: Callable[A,B], f2: Callable[B,C]): ... def __call__(self, in: A) -> C: ....
Best, Alfonso. _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: mrahtz@google.com

Please, note that the given approach with sequential `Callable` parameters does not work with: 1. Even the simplest Generic functions: https://mypy-play.net/?mypy=latest&python=3.10&gist=19fdad0cfac7f9f9e4b560804331f988 2. lambdas: https://mypy-play.net/?mypy=latest&python=3.10&gist=e7ab25fcbddaa5531ec296ead7caf3a1 вт, 17 авг. 2021 г. в 12:18, Matthew Rahtz via Typing-sig < typing-sig@python.org>:
It'd be nice if Sequential had the same type parameter structure as Linear, which we've been assuming will be generic in just the input and output types:
class Linear(Generic[In, Out]): def __call__(x: Tensor[Batch, In]) -> Tensor[Batch, Out]: ...
That would suggest:
class Sequential(Generic[A, C]): def __init__(self, f1: Callable[[A], B], f2: Callable[[B], C]): pass
I think the reason we thought this wouldn't work when discussing it in the tensor typing meeting was that it would rely on a type variable B that wasn't one of the type parameters to the class - but surprisingly, it seems to be fine in mypy: https://mypy-play.net/?mypy=latest&python=3.10&gist=e4b1d2d7b70d79f62eadeca3d711193d
On Mon, 16 Aug 2021 at 19:13, Alfonso L. Castaño < alfonsoluis.castanom@um.es> wrote:
Hi,
Context: during the typing meeting of this week Brandon presented the idea of introducing a Compose type operator, which takes a variadic of callables, checks that they compose, and returns the type of the composed function.
To follow-up on the discussion about Compose:
We were wondering what can be achieved nowadays without custom operators, just relying on overload. Let's think about the example of a compose function, we could do the following right?
from typing import Callable as Call
# Overloads def compose(f1: Call[A,B]) -> Call[A,B] def compose(f1: Call[A,B], f2: Call[B,C]) -> Call[A,C] def compose(f1: Call[A,B], f2: Call[B,C], f3: Call[C,D]) -> Call[A,D] ...
What is less obvious is how would we type a class like torch.nn.Sequential, which would look like this with the Compose operator:
class Sequential(Generic[A,*Ts]): def __init__(self, fs: *Ts) def __call__(self, in: A) -> Compose[*Ts]
Here we can't overload the class definition, so what can be done? We had a couple of ideas but they would not really work for a variable number of arguments because we can't overload. An example of those approaches would be:
class Sequential(Generic[A,B,C]): def __init__(self, f1: Callable[A,B], f2: Callable[B,C]): ... def __call__(self, in: A) -> C: ....
Best, Alfonso. _______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: mrahtz@google.com
_______________________________________________ Typing-sig mailing list -- typing-sig@python.org To unsubscribe send an email to typing-sig-leave@python.org https://mail.python.org/mailman3/lists/typing-sig.python.org/ Member address: n.a.sobolev@gmail.com

I think your generic function example should type check without errors. This appears to simply be a bug in mypy. The error it reports doesn't make sense (`error: Cannot infer type argument 3 of "Sequential"`) because `Sequential` doesn't take three type arguments. This sample type checks fine in pyright and produces the expected results. This technique also works fine for the first of your two lambda examples. The second one (`s2 = Sequential(lambda x: int(x), f1)`) is a problem because there's not enough type information to infer the type of TypeVar `A` from the lambda. The type of the lambda's input parameter `x` is unknown, and there's not enough context to infer it. I suppose it could be inferred to be `object`, but that's probably not what the caller wants in this case. In any case, I think this approach is viable. -Eric -- Eric Traut Contributor to Pyright & Pylance Microsoft Corp.

Very interesting comments! I think that if we would agree that is a bug in Mypy then I think that we have showed that we could type a compose operator with a linear number of overloads. In that case, I think that it would be interesting for libraries to first try to offer this feature through overloads until we identify enough situations where overloading is not really feasible and it would require a custom compose operator. Best, Alfonso.
participants (4)
-
Alfonso L. Castaño
-
Eric Traut
-
Matthew Rahtz
-
Никита Соболев