Yesterday I had an idea to create a units library tool (or add type hinting to an existing units tool, like pint) that can detect the erroneous addition and subtraction of incompatible dimensions (of unit-bearing values) at type checking time. For the purposes of engineering/scientific computing, this could be extremely powerful. Currently all such tools I know of only detect those kinds of errors at runtime. CAVEAT: I’m only a civil engineer and am in no way a professional programmer. But otoh I’ve been writing python for 10+ years now.
I am imagining some final code using that unit library could look like this:
from my_unit_library import N, m
x = 1*m
assert str(x.dimensionality) == "[Length]"
f = 3*N
assert str(f.dimensionality) == "[Length]*[Mass]/[Time]**2"
z = x + f # type checker throws error here because the types of the dimensionality for the two values aren't compatible
The type checker would know that the dimensions for x is [Length], and the dimensions of y is [Length]*[Mass]/[Time]**2 (which is a force), and that you can’t add those two unlike dimensionalities together.
I tried to write a proof of concept of this idea, but I think it isn’t possible with current python type checking tech.
Such a tool would need to know how to combine dimensions correctly when they are multiplied and divided. And it would need to know that order doesn’t matter when comparing dimensionality; for example, Mass X Length is the same as Length X Mass.
Thinking through it, I think all of this could be possible of there were some new type checking tools/“functions” added to typing. I’ll go over that at the end. First more about the motivation.
The idea behind all of this is one could create some kind of DimMultiply and also some kind of DimDivide type, so that you can write code like this (using 3.12 type hint syntax):
class Dimensionality[T]:
def __mul__[O](self, other: O) -> DimMultiply[T, O]:
...
def __truediv__[O](self, other: O) -> DimDivide[T, O]:
...
Unless I’m wrong: all physical dimension combinations in principle can be represented as a single numerator and a single denominator (both optional).
So DimDivide and DimMultiply would work something like this:
DimDivide[A, B]
# returns Tuple[Numerator[A], Denominator[B]]
DimMultiply[A, B]
# returns Tuple[Numerator[A, B]]
DimDivide[DimMultiply[A, B], C]
# returns Tuple[Numerator[A, B], Denominator[C]]
Above, Numerator and Denominator are not much more than aliases for Tuple, but with names to distinguish them as the numerator and denominator of the dimensions.
Then I could use that code like this:
class Length: ...
class Mass: ...
class Time: ...
# this would return Tuple[Numerator[Length], Denominator[Time, Time]]
Acceleration = DimDivide[Length, DimMultiply[Time, Time]]
# this would return Tuple[Numerator[Length, Mass], Denominator[Time, Time]]
Force = DimMultiply[Mass, Acceleration]
# this would return Tuple[Numerator[Length, Length]]
Area = DimMultiply[Length, Length]
# this would return Tuple[Numerator[Mass], Denominator[Length, Time, Time]]
Pressure = DimDivide[Force, Area]
In order to generalize this behavior across all kinds of dimension combinations, one would need to write some kind of DimMultiply and DimDivide function/type that magically creates the replacement types (“aliases”…??), and for the type checker to recognize the result coming from that function/type.
But unless I’m wrong, with current tools it isn’t possible to write a DimMultiply and DimDivide that will allow the type checker to understand that, for example, DimDivide[Force, Area] evaluates to an “alias” (maybe that’s the wrong word) for Tuple[Numerator[Mass], Denominator[Length, Time, Time]].
The algorithm for DimMultiply would be like this (DimDivide would be similar):
arguments: tuple_a_or_atomic, tuple_b_or_atomic
step 1: determine if a and b are tuples or atomics; if either is atomic, swap it to a single-member tuple inside of a Numerator[]
step 2: check to confirm that the only contents of the two tuples a and b are at most a single Numerator and a single Denominator
step 3: combine the a and b numerators, and a and b denominators, together
step 4: sort the numerator and the denominator
step 5: return a tuple of the numerator and denominator
There is probably more than one “thing” missing from the type hinting system that one would need to write this algorithm. And maybe the entire thing could be simplified/changed to get rid of some of the complications in the algorithm above, so that it is more possible with python’s existing type hinting tools.
However I think at minimum we would need a new type hinting “function” I will call Sorted[].
Sorted[] would receive a tuple of types as the argument, and return them as a new tuple in a sorted order. This allows for comparing objects with a Tuple tupe containing all the same types, even if they aren’t in the same order. I don’t think it matter what the order is, it just needs to be consistent.
We need this because the type checkers (correctly) care about the order of types inside of Tuples. But when checking dimensionality compatibility, the order doesn’t matter. So I suggest to address this, you could provide the ability to sort tuples and that this ability would allow a library like this to work: it would just sort all the numerators and all the denominators so they compare consistently.
Looking at the rest of the algorithm, there are probably other things we would need to make this possible… but I think this is enough for now.
What do people think? How much of this is possible with current type hint tools? Could it be done without adding anything? I’m pretty sure the answer to that last is no.