Typeclass

Here are the technical docs about typeclass and how to use it.

typeclass(signature)[source]

Function to define typeclasses.

The first and the simplest example of a typeclass is just its definition:

>>> from classes import typeclass
>>> @typeclass
... def example(instance) -> str:
...     '''Example typeclass.'''
...
>>> example(1)
Traceback (most recent call last):
...
NotImplementedError: Missing matched typeclass instance for type: int

In this example we work with the default implementation of a typeclass. It raise a NotImplementedError when no instances match. And we don’t yet have a special case for int, that why we fallback to the default implementation.

It works like a regular function right now. Let’s do the next step and introduce the int instance for the typeclass:

>>> @example.instance(int)
... def _example_int(instance: int) -> str:
...     return 'int case'
...
>>> example(1)
'int case'

Now we have a specific instance for int which does something different from the default implementation.

What will happen if we pass something new, like str?

>>> example('a')
Traceback (most recent call last):
...
NotImplementedError: Missing matched typeclass instance for type: str

Because again, we don’t yet have an instance of this typeclass for str type. Let’s fix that.

>>> @example.instance(str)
... def _example_str(instance: str) -> str:
...     return instance
...
>>> example('a')
'a'

Now it works with str as well. But differently. This allows developer to base the implementation on type information.

So, the rule is clear: if we have a typeclass instance for a specific type, then it will be called, otherwise the default implementation will be called instead.

Generics

We also support generic, but the support is limited. We cannot rely on type parameters of the generic type, only on the base generic class:

>>> from typing import Generic, TypeVar
>>> T = TypeVar('T')
>>> class MyGeneric(Generic[T]):
...     def __init__(self, arg: T) -> None:
...          self.arg = arg
...

Now, let’s define the typeclass instance for this type:

>>> @example.instance(MyGeneric)
... def _my_generic_example(instance: MyGeneric) -> str:
...     return 'generi' + str(instance.arg)
...
>>> example(MyGeneric('c'))
'generic'

This case will work for all type parameters of MyGeneric, or in other words it can be assumed as MyGeneric[Any]:

>>> example(MyGeneric(1))
'generi1'

In the future, when Python will have new type mechanisms, we would like to improve our support for specific generic instances like MyGeneric[int] only. But, that’s the best we can do for now.

Protocols

We also support protocols. It has the same limitation as Generic types. It is also dispatched after all regular instances are checked.

To work with protocols, one needs to pass is_protocol flag to instance:

>>> from typing import Sequence
>>> @example.instance(Sequence, is_protocol=True)
... def _sequence_example(instance: Sequence) -> str:
...     return ','.join(str(item) for item in instance)
...
>>> example([1, 2, 3])
'1,2,3'

But, str will still have higher priority over Sequence:

>>> example('abc')
'abc'

We also support user-defined protocols:

>>> from typing_extensions import Protocol
>>> class CustomProtocol(Protocol):
...     field: str
...
>>> @example.instance(CustomProtocol, is_protocol=True)
... def _custom_protocol_example(instance: CustomProtocol) -> str:
...     return instance.field
...

Now, let’s build a class that match this protocol and test it:

>>> class WithField(object):
...    field: str = 'with field'
...
>>> example(WithField())
'with field'

Remember, that generic protocols have the same limitation as generic types.

Return type

_Typeclass[~_TypeClassType, ~_ReturnType, ~_CallbackType]