Parametric Classes#

Construction#

The decorator @parametric can be used to create parametric classes:

from plum import dispatch, parametric


@parametric
class A:
    def __init__(self, x):
        self.x = x

You can create a version of A with a type parameter using __getindex__:

>>> A
<class 'A'>

>>> A[int]
<class 'A[int]'>

These types A[int] can be regarded as subclasses of A:

>>> issubclass(A[int], A)
True

We call A[int] a concrete parametric type. It is concrete because A[int] can be instantiated into an object:

>>> A[int](1)
<A[int] object at ...>

When you don’t instantiate a concrete parametric type, but try to instantiate A directly, the type parameter is automatically inferred from the argument:

>>> A(1)
<A[int] object at ...>

>>> A("1")
<A[str] object at ...>

>>> A(1.0)
 <A[float] object at ...>

You can use parametric types to perform dispatch:

@dispatch
def f(x: A):
    return "Just some A"


@dispatch
def f(x: A[int]):
    return "A has an integer!"


@dispatch
def f(x: A[float]):
    return "A has a float!"

Note that parametric types are covariant, which means that A[T1] is a subtype of A[T2] whenever T1 is a subtype of T2:

>>> from numbers import Number

>>> issubclass(A[int], A[Number])
True

For a concrete parametric type or an instance of a concrete parametric type, you can extract the type parameter with type_parameter:

>>> from plum import type_parameter

>>> type_parameter(A[int])
<class 'int'>

>>> type_parameter(A[int](1))
<class 'int'>

Customisation#

You can customise precisely how type parameters are inferred and instantiated by overriding certain @classmethods:

Class Method

What does it do?

__init_type_parameter__

Initialise the type parameter.

__infer_type_parameter__

Infer the type parameter from the arguments.

__le_type_parameter__

For a given left and right, check whether Type[left] is a subtype of Type[Right].

How these methods work is best described with an example. See also help(parametric) for information.

from plum import dispatch, parametric, type_parameter


@parametric
class NTuple:
    def __init__(self, *args):
        # Check that the arguments satisfy the type specification.
        n, t = type_parameter(self)
        if len(args) != n or any(not isinstance(arg, t) for arg in args):
            raise ValueError("Incorrect arguments!")

        self.args = args

    @classmethod
    @dispatch
    def __init_type_parameter__(self, n: int, t: type):
        """Check whether the type parameters are valid."""
        # In this case, we use `@dispatch` to check the validity of the type parameter.
        return n, t

    @classmethod
    def __infer_type_parameter__(self, *args):
        """Inter the type parameter from the arguments."""
        n = len(args)
        # For simplicity, take the type of the first argument! We could do something
        # more refined here.
        t = type(args[0])
        return n, t

    @classmethod
    def __le_type_parameter__(self, left, right):
        """Is `NTuple[left]` a subtype of `NTuple[right]`?"""
        n_left, t_left = left
        n_right, t_right = right
        return n_left == n_right and issubclass(t_left, t_right)

NTuple automatically infers an appropriate type parameter with __infer_type_parameter__:

>>> NTuple(10, 11, 12)
<NTuple[3, int] object at ...>

It also validates any given type parameter using __init_type_parameter__:

>>> NTuple[2, int]     # OK
<class 'NTuple[2, int]'>

>>> try: NTuple[2, "int"]   # Not OK
... except Exception as e: print(f"{type(e).__name__}: {e}")
NotFoundLookupError: `NTuple.__init_type_parameter__(<class 'NTuple'>, 2, 'int')` could not be  resolved...

>>> try: NTuple[None, int]  # Also not OK
... except Exception as e: print(f"{type(e).__name__}: {e}")
NotFoundLookupError: `NTuple.__init_type_parameter__(<class 'NTuple'>, None, <class 'int'>)` could not be resolved...

Given a valid type parameter, it validates the arguments:

>>> NTuple[2, int](10, 11)
<NTuple[2, int] object at ...>

>>> try: NTuple[2, int](10, 11, 12)
... except Exception as e: print(f"{type(e).__name__}: {e}")
ValueError: Incorrect arguments!

>>> try: NTuple[2, int](10, "11")
... except Exception as e: print(f"{type(e).__name__}: {e}")
ValueError: Incorrect arguments!

Finally, it implements the desired covariance:

>>> from numbers import Number

>>> issubclass(NTuple[2, int], NTuple[2, Number])
True

>>> issubclass(NTuple[2, int], NTuple[2, float])
False

>>> issubclass(NTuple[2, int], NTuple[3, int])
False

Kind#

Plum provides a convience parametric class Kind which you can use to quickly make a parametric wrapper object:

>>> from plum import Kind

>>> this = Kind["This"](1)

>>> this
<plum.parametric.Kind['This'] object at ...>

>>> this.get()
1

>>> that = Kind["That"]("some", "args", "here")

>>> that
<plum.parametric.Kind['That'] object at ...>

>>> that.get()
('some', 'args', 'here')

For example, you can use this in the following way:

from plum import dispatch


@dispatch
def i_expect_this(this: Kind["this"]):
    arg = this.get()
    ...


@dispatch
def i_expect_that(that: Kind["that"]):
    arg0, arg1, arg2 = that.get()
    ...

typing.Literal (and Val)#

To bring information from the object domain to the type domain use typing.Literal. Plum’s now-deprecated equivalent means is through the class plum.Val.

Example:

from typing import Literal
from plum import dispatch


@dispatch
def algorithm(setting: Literal["fast"], x):
    return "Running fast!"


@dispatch
def algorithm(setting: Literal["slow"], x):
    return "Running slowly..."
>>> algorithm("fast", 1)
'Running fast!'

>>> algorithm("slow", 1)
'Running slowly...'

Example: NDArray#

See here.