Skip to content

Commit b274a09

Browse files
committed
WIP
1 parent 59923e9 commit b274a09

File tree

3 files changed

+134
-2
lines changed

3 files changed

+134
-2
lines changed

classes/_typeclass.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,7 @@ def __call__(
416416
try:
417417
impl = self._dispatch_cache[instance_type]
418418
except KeyError:
419-
impl = self._dispatch(
419+
impl = self._dispatch( # TODO: impl, store_cache = self._dispatch
420420
instance,
421421
instance_type,
422422
) or self._default_implementation

docs/pages/concept.rst

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,142 @@ Example:
130130
>>> assert isinstance(argument, Some)
131131
>>> assert some(argument) == 2
132132
133-
.. note::
133+
.. warning::
134134

135135
It is impossible for ``mypy`` to understand that ``1`` has ``Some``
136136
type in this example. Be careful, it might break your code!
137137

138+
This example is not really useful on its own,
139+
because as it was said, it can break things.
140+
141+
Instead, we are going to learn about
142+
how this feature can be used to model
143+
your domain model precisely with delegates.
144+
145+
146+
Delegates
147+
---------
148+
149+
Let's say that you want to handle types like ``List[int]`` with ``classes``.
150+
The simple approach won't work, because Python cannot tell
151+
that some ``list`` is ``List[int]`` or ``List[str]``:
152+
153+
.. code:: python
154+
155+
>>> from typing import List
156+
157+
>>> isinstance([1, 2, 3], List[int])
158+
Traceback (most recent call last):
159+
...
160+
TypeError: Subscripted generics cannot be used with class and instance checks
161+
162+
We need some custom type inference mechanism:
163+
164+
.. code:: python
165+
166+
>>> from typing import List
167+
168+
>>> class _ListOfIntMeta(type):
169+
... def __instancecheck__(self, arg) -> bool:
170+
... return (
171+
... isinstance(other, list) and
172+
... all(isinstance(item, int) for item in arg
173+
... )
174+
175+
>>> class ListOfInt(List[int], metaclass=_ListOfIntMeta):
176+
... ...
177+
178+
Now we can be sure that our ``List[int]`` can be checked in runtime:
179+
180+
.. code:: python
181+
182+
>>> assert isinstance([1, 2, 3], ListOfInt) is True
183+
>>> assert isinstance([1, 'a'], ListOfInt) is False
184+
185+
And now we can use it with ``classes``:
186+
187+
.. code:: python
188+
189+
>>> from classes import typeclass
190+
191+
>>> @typeclass
192+
... def sum_all(instance) -> int:
193+
... ...
194+
195+
>>> @sum_all.instance(ListOfInt)
196+
... def _sum_all_list_int(instance: ListOfInt) -> int:
197+
... return sum(instance)
198+
199+
>>> your_list = [1, 2, 3]
200+
>>> if isinstance(your_list, ListOfInt):
201+
... sum_all(your_list)
202+
203+
This solution still has several problems:
204+
205+
1. Notice, that you have to use ``if isinstance`` or ``assert isinstance`` here.
206+
Because otherwise ``mypy`` won't be happy without it,
207+
type won't be narrowed to ``ListOfInt`` from ``List[int]``.
208+
This does not feel right.
209+
2. ``ListOfInt`` is very verbose, it even has a metaclass!
210+
3. There's a typing mismatch: in runtime ``your_list`` would be ``List[int]``
211+
and ``mypy`` thinks that it is ``ListOfInt``
212+
(a fake type that we are not ever using directly)
213+
214+
To solve all these problems we recommend to use ``phantom-types`` package.
215+
216+
First, you need to define a "phantom" type
217+
(it is called "phantom" because it does not exist in runtime):
218+
219+
.. code:: python
220+
221+
>>> from phantom import Phantom
222+
>>> from phantom.predicates import collection, generic
223+
224+
>>> class ListOfInt(
225+
... List[int],
226+
... Phantom,
227+
... predicate=collection.every(generic.of_type(int)),
228+
... ):
229+
... ...
230+
231+
>>> assert isinstance([1, 2, 3], ListOfInt)
232+
>>> assert type([1, 2, 3]) is list
233+
234+
Short, easy, and readable.
235+
236+
Now, we can define our typeclass with "phantom" type support:
237+
238+
.. code:: python
239+
240+
>>> from classes import typeclass
241+
242+
>>> @typeclass
243+
... def sum_all(instance) -> int:
244+
... ...
245+
246+
>>> @sum_all.instance(List[int], delegate=ListOfInt)
247+
... def _sum_all_list_int(instance: List[int]) -> int:
248+
... return sum(instance)
249+
250+
>>> assert sum_all([1, 2, 3]) == 6
251+
252+
That's why we need a ``delegate=`` argument here:
253+
we don't really work with ``List[int]``,
254+
we delegate all the runtime type checking to ``ListOfInt`` phantom type.
255+
256+
Performance considerations
257+
~~~~~~~~~~~~~~~~~~~~~~~~~~
258+
259+
Traversing the whole list to check that all elements
260+
are of the given type can be really slow.
261+
262+
You might need a different algorithm.
263+
Take a look at `beartype <https://github.com/beartype/beartype>`_.
264+
It promises runtime type checking with ``O(1)`` non-amortized worst-case time
265+
with negligible constant factors.
266+
267+
Take a look at their docs to learn more.
268+
138269
139270
Type resolution order
140271
---------------------

docs/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ tomlkit==0.7.2
1111

1212
# Dependencies of our project:
1313
typing-extensions==3.10.0.0
14+
phantom-types==0.9.1
1415

1516
# TODO: Remove this lock when we found and fix the route case.
1617
# See: https://github.com/typlog/sphinx-typlog-theme/issues/22

0 commit comments

Comments
 (0)