The problem
The current special casing of numerics is error-prone and leaves valid use cases inexpressible.
The varying numeric types all have methods that are unique to them.
“useless” methods have been added to help hide this (3.12, int
gained an is_integer()
method, which if you statically know you have an int
, you’d never call.)
There’s no way to express in the type system “This really only takes a float”. this has real performance consequences when arbitrary precision numerics are passed in some cases.
The places where people can run into this right now
The numeric types serve different purposes, and they interact with ffi very differently. This might seem innocuous to some, but tools that auto-generate bindings and signatures have no way to communicate they don’t handle int
as a type. In the other direction, some libraries wrap native code instead branch on the ffi type to “help” users, but this case creates worse performance for users who call with arbitrary precision integers in cases where a float would have been fine. A consumer of a library that intends to support both, but that itself only has a case for floats therefore has no way to communicate this intent in the type system to its downstream users.
Is this a bug and is it worth fixing?
This is an issue that has been raised repeatedly in tangent whenever people want to change the special case or reword it, while retaining it, but not remove this special case, but the argument is always “but that’s what’s specified, so it’s not a bug”, and the effort of change is spent on rewording what we currently have rather than fixing the issues at the root of what the cause of the error is.
So, it’s not a bug in any of the implementors. Sure. But does that mean it’s not a bug? The spec itself is in error, these aren’t valid substitutes of each other, and even if the interfaces between them were entirely shared with appropriate dummy methods, there would still be good cause to only accept certain numeric types due to the prevalence of ffi with numeric computation libraries.
Okay, what options do we have for fixing it?
Option 1: Just remove the special case
It would seem that the correct option should be the union of specific types you want to accept (eg. float | int
) or an abstract type if you intend to support anything that quacks like a duck, not the type system guessing that most people can probably take both, and then leaving reasonable use cases with no way to express their intent.
This would undoubtedly create a large amount of noise for people currently relying on the special case. This would be my preferred option, as it’s a single time breakage to fix this, and the existing special casing will certainly have an impact on other features people want (refinement types)
Option 2: A type checker directive
Adding a type checker directive into the spec, to not duck type numerics for things defined in that module would be the least disruptive for users, but might be the most disruptive for static type checkers and unfortunately leaves out runtime type checkers.
This gives people an off-switch for only the affected cases that are in the specification and applied to their code, (not a type checker flag that could be off in user code). I don’t particularly like the idea of more type comments that type checkers would need to understand or the impact this would have on runtime consumers.
This option would mean type checkers need to do extra work and maintain separate behavior for this case, and to track the actual type of things. if some variable x
is annotated as a float and later passed as an argument to code which disables this behavior, then x
must also have that behavior. This adds a flow analysis requirement to type checkers
Option 3: A type qualifier
eg. Exactly[float]
, where the type checker may not allow subtypes. This comes with many negative consequences, type checkers would need to track if they even know something is Exactly[float]
to begin with, and this would either need be incompatible with float
as a result or the presence of a single use of Exactly
needs to enable flow analysis that then treats all interacting uses of float
as Exactly[float]
This also comes with the implication that Exactly
might be valid on other types, even those without type checker special behavior. I don’t think the blanket disallowing of subtypes is a direction truly worth exploring, but I’m including this here anyway.
Option 4: a special typing type for “just this number type”
if there was something like typing.FloatNoDuckTyping
(and corresponding other numerics) this would have the same consequences of needing flow analysis and for this to bleed in based on use as exist in options 2 and 3, or comes with a situation where a float isn’t compatible with this.
Right now, I would say the only viable option is the first one here, removing the special case.
There are many other reasons why type checkers might have a use or need for flow-analysis, but options 2-4 either introduce a hard requirement of it or significantly break users as much as just removing the special case will by requiring users handle that propogation.
This hard requirement also presents new challenges for runtime type checkers, as presumably if they are only checking a specific annotation, they now need to predict future use to handle this.