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 @classmethod
s:
Class Method |
What does it do? |
---|---|
|
Initialise the type parameter. |
|
Infer the type parameter from the arguments. |
|
For a given |
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.