Comment by sevensor
3 months ago
Challenge accepted.
from dataclasses import dataclass
from typing import Protocol, Self, TypeVar
class Semigroup(Protocol):
def __add__(self, other: Self) -> Self:
...
T = TypeVar("T", bound=Semigroup)
def join_stuff(first: T, *rest: T) -> T:
accum = first
for x in rest:
accum += x
return accum
@dataclass
class C:
x: int
@dataclass
class D:
x: int
def __add__(self, other: Self) -> Self:
return type(self)(self.x + other.x)
@dataclass
class E:
x: int
def __add__(self, other: Self) -> Self:
return type(self)(self.x + other.x)
_: type[Semigroup] = D
_ = E
def doit() -> None:
print(join_stuff(1,2,3))
print(join_stuff((1,), tuple(), (2,)))
print(join_stuff("a", "b", "c"))
print(join_stuff(D(1), D(2)))
print(join_stuff(D(1), 3))
print(D(1) + 3) # caught by mypy
print(D(1) + E(3)) # caught by mypy
print(join_stuff(1,2,"a")) # Not caught by mypy
print(join_stuff(C(1), C(2))) # caught by mypy
doit()
Now, this doesn't quite work to my satisfaction. Mypy lets you freely mix and match values of incompatible types, and I don't know how to fix that. Basically, if you directly try to add a D and an int, mypy will yell at you, but there's no way I've found to insist that the arguments to join_stuff, in addition to being Semigroups, are all of the compatible types. It looks like mypy is checking join_stuff as if Semigroup were a concrete class, so once you're inside join_stuff, the actual types of the arguments become irrelevant.
However, it will correctly tell you that it can't accept arguments that don't define addition at all, and that's better than nothing.
Pretty cool that you got this far though!
I think at this point one starts to fight against Python, which wasn't designed with this in mind. But cool nonetheless.
Thanks! My approach is to stop once it starts to hurt, and figure out what I should expect the type checker to miss. The type system and I are both getting better at it as time goes by. It’s not perfect, but it’s way better than not having it.