I’m trying to figure out the way to write type annotations to represent a type nested[T,N] such that:
nested[T,0] = list[T]nested[T,1] = list[list[T]]nested[T,2] = list[list[list[T]]]- etc
Is there a good way of doing this?
I have hundreds of functions with signatures mostly being like:
def func1(p: nested[T,0]) -> T:
...
def func2(p: nested[T,0], q: nested[T,0]) -> nested[T,0]:
...
def func3(p: nested[T,N], q: nested[T,N], N: int) -> nested[T,N]:
...
def func4(p: nested[T,N], N: int) -> nested[T,N-1] | T:
...
There is no problem annotating nested[T,0] (i.e. list[T]) but the difficulty is that the functions taking nested[T,N] also need to call the nested[T,0] functions when N = 0.
It would be great but I imagine that it is too much to ask for a type checker to understand the connection between the integer parameter N and the nesting level in the types of p and q. I do want a checker to at least understand when different nesting levels are being mixed though like if p and q don’t have the same level but I have not succeeded in making that work yet. So far I have come up with this:
dup: TypeAlias = list[T]
dmp: TypeAlias = list["dmp[T]"]
Then I can use dup as nested[T,0] and dmp as nested[T,N] but there is no way to distinguish between nested[T,N] and nested[T,N-1]. This definition of dmp is of an infinitely nested type so there is no distinction between nesting levels and no understanding of the relationship between dmp and dup.
Here is a complete demo of the sort of code I have and what I have come up with so far for the annotations:
from __future__ import annotations
from typing import TYPE_CHECKING, TypeAlias, TypeVar
if TYPE_CHECKING:
T = TypeVar("T")
dup: TypeAlias = list[T]
dmp: TypeAlias = list["dmp[T]"]
def _dup(p: dmp[T], /) -> dup[T]: ...
def _dmp(p: dup[T], /) -> dmp[T]: ...
else:
def _dup(p, /): return p
def _dmp(p, /): return p
def dup_mul(c: int, p: dup[int]) -> dup[int]:
"""Multiply a dense univariate polynomial by a constant."""
return [a * c for a in p]
def dmp_mul(c: int, p: dmp[int], lev: int) -> dmp[int]:
"""Multiply a dense multivariate polynomial by a constant."""
if not lev:
return _dmp(dup_mul(c, _dup(p)))
else:
return [dmp_mul(c, pi, lev - 1) for pi in p]
p = [_dmp([1, 2, 3]), _dmp([2, 4, 6])]
print(dmp_mul(3, p, 1))
This type checks fine and a checker can pick up on various errors like if the elements in the list are not actually int or if you use dup_mul when you should use dmp_mul etc. However there is no checking/understanding of the nesting levels so you could do e.g. p + p[0] and a type checker would not recognise that the types are inconsistent. Also I need to call redundant do-nothing functions _dup and _dmp to “convert” between the two types at the base of recursion.