beartype / numerary

A pure-Python codified rant aspiring to a world where numbers and types can work together.
https://posita.github.io/numerary/latest/
Other
39 stars 1 forks source link

`TypeVar(…, bound=RealLike)` creates problems #10

Closed posita closed 2 years ago

posita commented 2 years ago

First hinted at by @antonagestam in https://github.com/antonagestam/phantom-types/pull/179#issue-1111676009, which led to an interesting work-around that numerary probably shouldn't impose, if it can avoid it.

posita commented 2 years ago

@antonagestam, I still have no idea know why RealLike[IntegralLike[int]] works for your use case. 😕 That's a head-scratcher. 😊

I tried the following with some success, although I'm not sure if this does what you want:

diff --git a/src/phantom/interval.py b/src/phantom/interval.py
index e795106..55db040 100644
--- a/src/phantom/interval.py
+++ b/src/phantom/interval.py
@@ -26,7 +26,6 @@ from __future__ import annotations
 from typing import Any
 from typing import TypeVar

-from numerary.types import IntegralLike
 from numerary.types import RealLike
 from typing_extensions import Final
 from typing_extensions import Protocol
@@ -37,12 +36,12 @@ from .predicates import interval
 from .schema import Schema
 from .utils import resolve_class_attr

-N = TypeVar("N", bound=RealLike[IntegralLike[int]])
+N = TypeVar("N", covariant=True)
 Derived = TypeVar("Derived", bound="Interval")

 class IntervalCheck(Protocol):
-    def __call__(self, a: N, b: N) -> Predicate[N]:
+    def __call__(self, a: RealLike[N], b: RealLike[N]) -> Predicate[RealLike[N]]:
         ...

diff --git a/src/phantom/predicates/base.py b/src/phantom/predicates/base.py
index d13a76e..10c01b7 100644
--- a/src/phantom/predicates/base.py
+++ b/src/phantom/predicates/base.py
@@ -1,6 +1,10 @@
-from typing import Callable
 from typing import TypeVar

+from typing_extensions import Protocol
+
 T = TypeVar("T", bound=object, contravariant=True)

-Predicate = Callable[[T], bool]
+
+class Predicate(Protocol[T]):
+    def __call__(self, __: T, /) -> bool:
+        ...

I also tried this:

diff --git a/src/phantom/interval.py b/src/phantom/interval.py
index e795106..a62fd3d 100644
--- a/src/phantom/interval.py
+++ b/src/phantom/interval.py
@@ -26,7 +26,6 @@ from __future__ import annotations
 from typing import Any
 from typing import TypeVar

-from numerary.types import IntegralLike
 from numerary.types import RealLike
 from typing_extensions import Final
 from typing_extensions import Protocol
@@ -37,11 +36,11 @@ from .predicates import interval
 from .schema import Schema
 from .utils import resolve_class_attr

-N = TypeVar("N", bound=RealLike[IntegralLike[int]])
+N = TypeVar("N", bound=RealLike, contravariant=True)
 Derived = TypeVar("Derived", bound="Interval")

-class IntervalCheck(Protocol):
+class IntervalCheck(Protocol[N]):
     def __call__(self, a: N, b: N) -> Predicate[N]:
         ...

In both cases, I can eliminate the error you got, but I get a new one:

src/phantom/base.py:162: error: Too many arguments for "__init_subclass__" of "object"  [call-arg]
            super().__init_subclass__(**kwargs)
            ^

Again, I'm not sure I follow everything that code does, nor am I sure if any of the above is helpful. I'll continue to look into this, but if there's a way to reduce it, that might help?

antonagestam commented 2 years ago

Hmm, that change doesn't really make sense to me. The original signature def __call__(self, a: N, b: N) -> Predicate[N] reads like, given two values that are of the same subtype of RealLike, return a predicate that takes the same type as argument. So given a: float the signature requires b: float and Predicate[float]. Returning Predicate[int] would be a type error for that, and passing a: int, b: float is also a type error.

But the new signature def __call__(self, a: RealLike[N], b: RealLike[N]) -> Predicate[RealLike[N]], reads something like given two values that are of some subtypes of RealLike (not necessarily the same, this signature would accept a mix of float and ints), return a predicate that also takes some RealLike (again, not necessarily the same). The only thing that's enforced to be shared between a, b and the return value here is the operations that are typed to return N ...

So I don't think I want to apply that change to the type var usage.

Is changing Predicate from a Callable to a Protocol related?

In both cases, I can eliminate the error you got, but I get a new one:

That's a known mypy bug, I think it has been fixed recently: https://github.com/python/mypy/issues/4660 Which mypy version are you running with?

posita commented 2 years ago

I finally have an update! I've since made some changes to numerary, which comes with some caveats…

The bad news: numerary has dropped support for Python 3.7. (Its support of 3.7 was illusory anyway, but now it no longer lies about it.) I know that phantom-types signals compatibility with 3.7, so this may be a deal killer? I'm not sure.

Other news: numerary's caching Protocol implementation now resides in beartype, which numerary now requires.

The good news: It appears this is no longer an issue, at least not with Mypy 0.960 (and possibly other recent versions). Here's my diff to antonagestam/phantom-types@0461b69c5f38579183b47d40c139e914314e7cd9 :

diff --git a/src/phantom/interval.py b/src/phantom/interval.py
index 0abe9ba..31dbe6e 100644
--- a/src/phantom/interval.py
+++ b/src/phantom/interval.py
@@ -26,7 +26,6 @@ from __future__ import annotations
 from typing import Any
 from typing import TypeVar

-from numerary.types import IntegralLike
 from numerary.types import RealLike
 from typing_extensions import Final
 from typing_extensions import Protocol
@@ -37,7 +36,7 @@ from .predicates import interval
 from .schema import Schema
 from .utils import resolve_class_attr

-N = TypeVar("N", bound=RealLike[IntegralLike[int]])
+N = TypeVar("N", bound=RealLike)
 Derived = TypeVar("Derived", bound="Interval")

Here's how it plays (from the phantom-types repo root):

$ pip list | grep numerary
numerary           0.4.0
$ mypy --version
mypy 0.960 (compiled: yes)
$ mypy src/phantom/interval.py
Success: no issues found in 1 source file
$ pre-commit run mypy --all-files
mypy.....................................................................Passed

I'm pretty sure this was the result of changes to Mypy rather than changes to numerary, but I don't know for sure. Either way, I think we can mark this as closed. @antonagestam, if that doesn't feel right, let me know and we can reopen and discuss further.