Python type system experiments.
Push the envelope as far as you can! ...and then give up and use cast(), because we don't have applicative functors or higher kinded types
This commit is contained in:
parent
c7fe539ac6
commit
c5eaba88b7
@ -1,4 +1,4 @@
|
|||||||
from typing import Any, Generic, Hashable, TypeVar, Callable
|
from typing import Any, Hashable, TypeVar, Callable
|
||||||
|
|
||||||
from gears.effect import effect_of
|
from gears.effect import effect_of
|
||||||
from gears.gear import Gear
|
from gears.gear import Gear
|
||||||
@ -10,7 +10,7 @@ T = TypeVar("T", bound=Hashable)
|
|||||||
def connect(
|
def connect(
|
||||||
source: Gear[S],
|
source: Gear[S],
|
||||||
to_target_val: Callable[[S], T],
|
to_target_val: Callable[[S], T],
|
||||||
to_source: Callable[[S, T], S] | None = None,
|
to_source: Callable[[S, T], S],
|
||||||
) -> Gear[T]:
|
) -> Gear[T]:
|
||||||
target = Gear(to_target_val(source()))
|
target = Gear(to_target_val(source()))
|
||||||
|
|
||||||
|
@ -1,47 +1,31 @@
|
|||||||
|
from typing import Any, Callable, cast
|
||||||
|
|
||||||
from gears import Gear
|
from gears import Gear
|
||||||
from typing import Any, Generic, Hashable, TypeVar, Callable, TypeVarTuple
|
from .wrap_aware_tuple import WrapAwareTuple
|
||||||
|
|
||||||
|
class Effect[*Ts]:
|
||||||
class Effect:
|
def __init__(self, fn: Callable[[*Ts], None], gears: WrapAwareTuple[Gear, *Ts]):
|
||||||
def __init__(self, fn: Callable[..., None], *gears: Gear):
|
|
||||||
self._fn = fn
|
|
||||||
self._gears = gears
|
|
||||||
for gear in gears:
|
for gear in gears:
|
||||||
gear.effects.append(self)
|
gear.effects.append(self)
|
||||||
|
self._fn = fn
|
||||||
|
self._gears = gears
|
||||||
|
|
||||||
self.on_change(gears[0]())
|
self.on_change()
|
||||||
|
|
||||||
def on_change(self, _value: Any):
|
def on_change(self):
|
||||||
self._fn(*(gear() for gear in self._gears))
|
self._fn(*self._gears.unwrap(lambda x: x()))
|
||||||
|
|
||||||
|
def effect_of[*Ts, *G_Ts](
|
||||||
|
*gears: Gear[Any]
|
||||||
|
) -> Callable[[Callable[[*Ts], None]], Effect]:
|
||||||
|
# No deduction, no tears, only cast now
|
||||||
|
ts_gears = cast(WrapAwareTuple[Gear, *Ts], WrapAwareTuple.prewrapped(gears))
|
||||||
|
return effect_of_typechecked(ts_gears)
|
||||||
|
|
||||||
T0 = TypeVar("T0", bound=Hashable)
|
def effect_of_typechecked[*Ts](
|
||||||
T1 = TypeVar("T1", bound=Hashable)
|
gears: WrapAwareTuple[Gear, *Ts]
|
||||||
T2 = TypeVar("T2", bound=Hashable)
|
) -> Callable[[Callable[[*Ts], None]], Effect]:
|
||||||
|
def decorator(fn: Callable[[*Ts], None]) -> Effect:
|
||||||
# Ts = TypeVarTuple("Ts", bound=Hashable)
|
return Effect(fn, gears)
|
||||||
|
|
||||||
|
|
||||||
def effect_of(g0: Gear[T0]) -> Callable[[Callable[[T0], None]], Effect]:
|
|
||||||
def decorator(fn: Callable[[T0], None]) -> Effect:
|
|
||||||
return Effect(fn, g0)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def effect_of_2(
|
|
||||||
g0: Gear[T0], g1: Gear[T1]
|
|
||||||
) -> Callable[[Callable[[T0, T1], None]], Effect]:
|
|
||||||
def decorator(fn: Callable[[T0, T1], None]) -> Effect:
|
|
||||||
return Effect(fn, g0, g1)
|
|
||||||
|
|
||||||
return decorator
|
|
||||||
|
|
||||||
|
|
||||||
def effect_of_3(
|
|
||||||
g0: Gear[T0], g1: Gear[T1], g2: Gear[T2]
|
|
||||||
) -> Callable[[Callable[[T0, T1, T2], None]], Effect]:
|
|
||||||
def decorator(fn: Callable[[T0, T1, T2], None]) -> Effect:
|
|
||||||
return Effect(fn, g0, g1, g2)
|
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
from typing import Any, Generic, Hashable, TypeVar, Callable
|
from typing import Any, Hashable, TypeVar, TYPE_CHECKING
|
||||||
|
|
||||||
T = TypeVar("T", bound=Hashable)
|
T = TypeVar("T", bound=Hashable)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from gears.effect import Effect
|
||||||
|
|
||||||
class Gear(Generic[T]):
|
class Gear[T]():
|
||||||
def __init__(self, value: T):
|
def __init__(self, value: T):
|
||||||
self._value = value
|
self._value = value
|
||||||
self.effects = []
|
self.effects: list['Effect'] = []
|
||||||
|
|
||||||
def get(self) -> T:
|
def get(self) -> T:
|
||||||
return self._value
|
return self._value
|
||||||
@ -19,4 +21,4 @@ class Gear(Generic[T]):
|
|||||||
return
|
return
|
||||||
self._value = value
|
self._value = value
|
||||||
for effect in self.effects:
|
for effect in self.effects:
|
||||||
effect.on_change(value)
|
effect.on_change()
|
||||||
|
90
gears/wrap_aware_tuple.py
Normal file
90
gears/wrap_aware_tuple.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
|
||||||
|
from typing import cast, Callable, reveal_type, TYPE_CHECKING
|
||||||
|
|
||||||
|
# no-op "Wrap" type
|
||||||
|
type Identity[X] = X
|
||||||
|
|
||||||
|
class WrapAwareTuple[Wrap, *Ts](tuple[Wrap, ...]):
|
||||||
|
'''
|
||||||
|
Wrap is a wrapper type around each of the types in Ts, in order.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
a: Wrapped[Wrap, int, str, float] = Wrapped(Wrap[int], Wrap[str], Wrap[float])
|
||||||
|
|
||||||
|
This lets us keep track of type transformations, starting from the Identity mapping.
|
||||||
|
'''
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def id[*NewTs](*args: *NewTs) -> 'WrapAwareTuple[Identity, *NewTs]':
|
||||||
|
'''
|
||||||
|
Create a new WrapAwareTuple, using the Identity psuedo-wrapper.
|
||||||
|
'''
|
||||||
|
return WrapAwareTuple[Identity, *NewTs](args)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def prewrapped[NewWrap, *NewTs](args: tuple[NewWrap, ...]) -> 'WrapAwareTuple[NewWrap, *NewTs]':
|
||||||
|
'''
|
||||||
|
Create a new WrapAwareTuple, using the given wrapper type.
|
||||||
|
'''
|
||||||
|
return WrapAwareTuple[NewWrap, *NewTs](args)
|
||||||
|
|
||||||
|
def wrap[NewWrap](self, mapper: Callable[[Wrap], NewWrap]) -> 'WrapAwareTuple[NewWrap, *Ts]':
|
||||||
|
'''
|
||||||
|
Wrap all existing elements with a new wrapping function.
|
||||||
|
|
||||||
|
This function must unwrap the current wrapping type, if non-Identity, and apply a new one.
|
||||||
|
|
||||||
|
To pop the wrapped value from a tuple wrapper:
|
||||||
|
>>> Wrapped[tuple, int](3).wrap[Identity](lambda x: x[0])
|
||||||
|
|
||||||
|
To wrap an unwrapped value into a tuple:
|
||||||
|
>>> Wrapped[Identity, int](3).wrap(tuple)
|
||||||
|
'''
|
||||||
|
# pyright supports Union[*Ts], but we still can't do applicative functor things with it
|
||||||
|
return WrapAwareTuple[NewWrap, *Ts](mapper(v) for v in self)
|
||||||
|
|
||||||
|
def unwrap(self, mapper: Callable[[Wrap], Identity]) -> tuple[*Ts]:
|
||||||
|
'''
|
||||||
|
Unwrap all elements in the tuple, returning a normal tuple.
|
||||||
|
|
||||||
|
This is the same as calling wrap with the appropriate wrapper, except we simplify the signature and
|
||||||
|
return type for this case.
|
||||||
|
'''
|
||||||
|
return cast(tuple[*Ts], self.wrap(mapper))
|
||||||
|
|
||||||
|
def list_fmap[*Ts](t: tuple[*Ts]):
|
||||||
|
match t:
|
||||||
|
case (head, *tail):
|
||||||
|
return ([head], *list_fmap(tuple(tail)))
|
||||||
|
case _:
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
def _fmap_scratchpad():
|
||||||
|
reveal_type(list_fmap((1,"s",3.0)))
|
||||||
|
# Type of "list_fmap((1, "s", 3))" is "tuple[list[*tuple[int, str, float]], *tuple[list[*tuple[Unknown, ...]] | Unknown, ...]] | tuple[()]"
|
||||||
|
# ...wanted to get "tuple[list[int], list[str], list[float]]"; RIP
|
||||||
|
|
||||||
|
def _wrapaware_scratchpad():
|
||||||
|
|
||||||
|
atom = WrapAwareTuple.id(3)
|
||||||
|
reveal_type(atom)
|
||||||
|
# Type of "atom" is "Wrapped[Unknown, int]"
|
||||||
|
# (variable) atom: Wrapped[Identity[Unknown], int]
|
||||||
|
|
||||||
|
compound_3 = WrapAwareTuple.id(3, "str", 9.0)
|
||||||
|
reveal_type(compound_3)
|
||||||
|
# Type of "compound_3" is "Wrapped[Unknown, int, str, float]"
|
||||||
|
# (variable) compound_3: Wrapped[Identity[Unknown], int, str, float]
|
||||||
|
|
||||||
|
tupled_compound_3 = compound_3.wrap(tuple)
|
||||||
|
reveal_type(tupled_compound_3)
|
||||||
|
# Type of "tupled_compound_3" is "Wrapped[tuple[Unknown, ...], int, str, float]"
|
||||||
|
# (variable) tupled_compound_3: Wrapped[tuple[Identity[Unknown], ...], int, str, float]
|
||||||
|
|
||||||
|
restored_compound_3 = tupled_compound_3.wrap(lambda x: x[0])
|
||||||
|
reveal_type(restored_compound_3)
|
||||||
|
# Type of "restored_compound_3" is "Wrapped[Unknown, int, str, float]"
|
||||||
|
# (variable) restored_compound_3: Wrapped[Unknown, int, str, float]
|
@ -3,7 +3,7 @@ from typing import NamedTuple
|
|||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
from gears import Gear
|
from gears import Gear
|
||||||
from gears.connections import connect
|
from gears.connections import connect
|
||||||
from gears.effect import effect_of, effect_of_2
|
from gears.effect import effect_of
|
||||||
|
|
||||||
|
|
||||||
def test_get_set():
|
def test_get_set():
|
||||||
@ -35,7 +35,7 @@ def test_effect_of_2():
|
|||||||
|
|
||||||
last_arg: tuple[str, int] | None = None
|
last_arg: tuple[str, int] | None = None
|
||||||
|
|
||||||
@effect_of_2(g1, g2)
|
@effect_of(g1, g2)
|
||||||
def value_changed(v1: str, v2: int):
|
def value_changed(v1: str, v2: int):
|
||||||
nonlocal last_arg
|
nonlocal last_arg
|
||||||
last_arg = (v1, v2)
|
last_arg = (v1, v2)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user