← Back to context

Comment by the_af

3 months ago

But there was a conceivable way (maybe not in Python) to make a `slow_add` function very generic, yet only be defined over structures where any conceivable `+` operation is defined.

You just have to say the type implements Semigroup.

Yes, this would work if the arguments are lists, or integers, or strings. And it won't pass the typecheck for arguments that are not Semigroups.

It may not work with Python, but only because it's designers weren't initially interested in typechecking.

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.