dougmercer / signified

A Python library for reactive programming (with kind-of working type narrowing).
https://dougmercer.github.io/signified/
BSD 3-Clause "New" or "Revised" License
50 stars 3 forks source link

Investigate mypy type narrowing issues #19

Open laundmo opened 1 month ago

laundmo commented 1 month ago

I've recreated a simplified example of the most core type narrowing issue which works in mypy. I honestly can't fully tell what the difference is between my simplified version and the main one regarding this specific issue (unref/flattening).

Mainly just wanted to investigate this interesting typing issue. I'm probably not going to put in more work than this, so here's what I got. This works with both pyright and mypy.

Please note that my version doesn't require any inheriting of property methods or variable types at all, only using the classes to be able to take a generic (kind of like PhantomData in Rust). You can simply inherit Flattener[T] and use value: Nested[T] in any class to be able to flatten a nested value.

from typing import (
    Generic,
    TypeAlias,
    TypeVar,
    Union,
    cast,
    reveal_type,
)

T = TypeVar("T")

# base class which holds the generic value of the deeper nesting level
# it should be passed Deep[Nested[T]] instead of Deep[T]
class Deep(Generic[T]):
    pass

# base class which hodls the generic value of the shallower nesting level
# this should be passed Shallow[T] instead of Shallow[Nested[T]]
class Shallow(Generic[T]):
    pass

# type alias for nested values
# uses Deep since thats the one which should contain Nested
Nested: TypeAlias = Union[T, Deep["Nested[T]"]]

# where the magic happenes
# this is essentially the inverse of the above definition.
# I think of it like this:
# the above Nested type alias definition essentially does
# T = Deep[Deep[...Deep[T]...]]
# This Flattener seems to do:
# Deep[Deep[...Deep[T]...]] = Shallow[T]
# thanks to multiple inheritance "resolving" T fully for the first argument
# before moving on to the second
class Flattener(Deep[Nested[T]], Shallow[T]):
    pass

# we can then use Flattener in any class which should be able to output
# the flattened result of a Nested input.
class Variable(Flattener[T]):
    def __init__(self, initial: Nested[T]) -> None:
        self._value = initial

    @property
    def value(self) -> T:
        urefed = self._value
        while isinstance(urefed, Variable):
            urefed = urefed._value
        return cast(T, urefed)

        # recursive implementation: (easier to understand for me, but needs a type:ignore)
        # urefed = self._value
        # if isinstance(urefed, Variable):
        #     return urefed.value  # type: ignore
        # return cast(T, urefed)

a = Variable(1)
reveal_type(a.value)
b = Variable(a)
reveal_type(b.value)
c = Variable(b)
reveal_type(c.value)
print(b.value)
t.py:66: note: Revealed type is "builtins.int"
t.py:68: note: Revealed type is "builtins.int"
t.py:70: note: Revealed type is "builtins.int"
Success: no issues found in 1 source file
dougmercer commented 3 weeks ago

Hey @laundmo -- I took a stab at implementing your suggestion in #20 .

Some surprising mypy behavior bubbled up. Feel free to check it out if you're interested!