Replies: 3 comments 3 replies
-
As you likely surmised, I've struggled with shoehorning problems like this into Mypy quite a bit. I've spent a fair amount of casual time stewing on this, and my current thinking is that numerical hierarchies are frustrating our ability to make forward progress. I think a better area to focus on (which Mypy can't currently support) is some way to define algorithm output types as a function of their input types without having to enumerate all possibilities. Something like this (although I think there could be some hefty syntax improvements): TrueDivParamA = TypeVar("TrueDivParamA")
TrueDivParamB = TypeVar("TrueDivParamB")
TrueDivResult = DerivedTypeVar("TrueDivResult") # <- sort of like a generic, but requires context to be useful
class TrueDiv(Protocol[TrueDivParamA, TrueDivParamB], derived_types=[TrueDivResult]):
def __truediv__(self: TrueDivParamA, other: TrueDivParamB) -> TrueDivResult: # <- this provides the context
...
# similarly define, e.g., MulParamA, MulParamB, MulResult, etc.
MyAlgoParamA = TypeVar("MyAlgoParamA")
MyAlgoParamB = TypeVar("MyAlgoParamB")
# The following says that the MyAlgoOutput type reflects the type of
# whatever you'd get if you called
# __mul__(__truediv__(obja: MyAlgoParamA, objb: MyAlgoParamB), objc: int)
MyAlgoOutput = MulResult[TrueDivResult[MyAlgoParamA, MyAlgoParamB], int]
# Then we could define a generic callable whose output type depends on
# its input type. Protocols would have to support something similar.
MyAlgo = Callable[[MyAlgoParamA, MyAlgoParamB], MyAlgoOutput]
# Ideally we could do things like this:
class Foo(MyAlgo[A, B]):
def algo(obja: A, objb: B):
return (obja / objb) * 1 # <- inferred return type of Foo.algo is MulResult[TrueDivResult[A, B], int]
reveal_type(Foo().algo(2, Fraction(8)) # -> fractions.Fraction
reveal_type(Foo().algo(2.1, Fraction(8)) # -> float (Amidst various life stuff) I'm still wrestling with details (which is why I've been absent from that discussions.python.org thread). My intuition is that if we could express the above, we wouldn't need hierarchies like the numeric tower. They may actually just get in the way. One issue is that One other (unsolved) problem how to reconcile such a thing with Yours might be a good test case for whether my intuition holds for user-defined types? |
Beta Was this translation helpful? Give feedback.
-
Yeah, I can definitely relate. I've recently spent a bunch of time just figuring out how to implement a mapping type in a way that is both easy for humans to read and satisfactory to mypy & pyright in strict mode. I'm writing library code, so I don't want to create unnecessary gotchas for end users. That's surprisingly difficult to do for something as ubiquitous as the dict/mapping constructor interface (which accepts a mapping or iterable pairs). As I'm modeling a probability mass function, the pairs/mappings are from discrete values to probabilities. I would like to offer shorthand notation for things like equal shares or dice ranges. For example, I'd like for all of these things to have the same meaning:
The first two are the standard
This doesn't directly relate to the numeric tower stuff, but I think it's the same kind of thing you're struggling with around overloading. Currently, I'm leaning toward making things simpler and more explicit. That is, I don't think it's necessarily a bad thing to use explicit adaptors instead of convenience overloads. Like, in the example above, I wanted the option to abbreviate For similar reasons, I have a bunch of messy So far I've found it easier to just accept the builtin quirks and assume that all probabilities are either |
Beta Was this translation helpful? Give feedback.
-
As an aside, C++ really tried to do the generic algorithm thing too. I haven't followed the progress of C++ for a long time, but I recall there being similar stumbling blocks along the way. That language has very different design goals of course, with much more behavioral overloading on type, rather Python's compromise between static type linting and runtime duck typing. I think both languages ended up "good enough" for everyday use, but they're both conceptually ugly for library writers who want to extend or unify abstractions. In library code there's always the tension between wanting to handle more general cases but only having the resources to build and test the most common ones. That ends up reinforcing the ecosystem status quo, as only the common cases are actually safe to use in production code. |
Beta Was this translation helpful? Give feedback.
-
So as I mentioned in posita/dyce#10, I'm working on a game analysis library, currently focusing on Warhammer combat,1 and I've been struggling with the numeric typing. Like, the Python numeric tower is enough of a headache on its own, and then I'm trying to extend it to handle ideas like "the Attacks characteristic is usually a number like 5 but sometimes the result of a die roll like 1d6" or "a weapon's Strength characteristic can be an integer, or it can be a modifier to the wielder's Strength characteristic, or it can multiply the wielder's Strength." Then I need to deal with the subtle differences between Warhammer Age of Sigmar and Warhammer 40,000. I'm managing so far with a tangle of dataclasses and subtypes and factory functions, but it's getting messy. So far it's been easier to handle the I/O than the actual data representation, ha.
I'm going to look through your work on
dyce
andnumerary
for whatever insight that brings me. Really happy to have stumbled onto your work. In retrospect it makes sense that I'd run across somebody working on similar applications while struggling with similar problems.Footnotes
https://github.com/bszonye/bones/pull/2 ↩
Beta Was this translation helpful? Give feedback.
All reactions