Pattern matching¶
The libndtypes implementation of datashape is dynamically typed with strict type checking. Static type checking of datashape would be far more complex, since datashape allows dependent types [1], i.e. types depending on values.
Dynamic pattern matching is used for checking function arguments, return values, broadcasting and general array functions.
Again, we will be using the ndtypes
module included in
ndtypes to demonstrate
datashape pattern matching. The rest of this document assumes that the
ndtypes
module has been imported:
from ndtypes import ndt
General notes¶
ndt
instances have a match
method for determining
whether the argument type is compatible with the instance type. The match
succeeds if and only if the set of types described by the right hand side
is a subset of the set of types described by the left hand side.
Simple example¶
>>> p = ndt("Any")
>>> c = ndt("int32")
>>> p.match(c)
True
Non-commutativity¶
From the above definition it follows that pattern matching is not commutative:
>>> p = ndt("int32")
>>> c = ndt("Any")
>>> p.match(c)
False
Concrete matching¶
Much like members of the alphabet in regular expressions, concrete types match themselves:
>>> p = ndt("int32")
>>> c = ndt("int32")
>>> p.match(c)
True
>>> p = ndt("10 * float64")
>>> c = ndt("10 * float32")
>>> p.match(c)
False
Type kinds¶
Type kinds are named subsets of types.
Unlike dtype variables, matching type kinds does not require that a well defined substitution exists. Two instances of a type kind can match different types:
>>> p = ndt("(Any, Any)")
>>> c = ndt("(float64, int32)")
>>> p.match(c)
True
Any¶
The Any type kind is the most general and describes the set of all types.
Here’s how to match a dtype against the set of all types:
>>> p = ndt("Any")
>>> c = ndt("int32")
>>> p.match(c)
True
This matches an array type against the set of all types:
>>> p = ndt("Any")
>>> c = ndt("10 * 5 * { v: float64, t: float64 }")
>>> p.match(c)
True
Scalar¶
The Scalar type kind stands for the set of all scalars.
int32
is a member of the set of all scalars:
>>> p = ndt("Scalar")
>>> c = ndt("int32")
>>> p.match(c)
True
Unlike with type variables, different types match a type kind:
>>> p = ndt("(Scalar, Scalar)")
>>> c = ndt("(uint8, float64)")
>>> p.match(c)
True
FixedString¶
The set of all fixed string types.
>>> p = ndt("FixedString")
>>> c = ndt("fixed_string(100)")
>>> p.match(c)
True
>>> p = ndt("FixedString")
>>> c = ndt("fixed_string(100, 'utf16')")
>>> p.match(c)
True
>>> p = ndt("FixedString")
>>> c = ndt("string")
>>> p.match(c)
False
FixedBytes¶
The set of all fixed bytes types.
>>> p = ndt("FixedBytes")
>>> c = ndt("fixed_bytes(size=100)")
>>> p.match(c)
True
>>> p = ndt("FixedBytes")
>>> c = ndt("fixed_bytes(size=100, align=2)")
>>> p.match(c)
True
>>> p = ndt("FixedBytes")
>>> c = ndt("bytes(align=2)")
>>> p.match(c)
False
Dimension kinds¶
Dimension kinds stand for the set of all instances of the respective kind.
Fixed¶
The set of all instances of the fixed dimension kind.
>>> p = ndt("Fixed * 20 * bool")
>>> c = ndt("10 * 20 * bool")
>>> p.match(c)
True
>>> p = ndt("Fixed * Fixed * bool")
>>> c = ndt("var * var * bool")
>>> p.match(c)
False
Dtype variables¶
dtype variables are placeholders for dtypes. It is important to note that they are not general type variables. For example, they do not match array types, a concept which is used in general array functions [2], whose base cases may operate on a dtype.
This matches a record against a single dtype variable:
>>> p = ndt("T")
>>> c = ndt("{v: float64, t: float64}")
>>> p.match(c)
True
Match against several dtype variables in a tuple type:
>>> p = ndt("T")
>>> c = ndt("(int32, int32, bool)")
>>> p.match(c)
True
>>> p = ndt("(T, T, S)")
>>> c = ndt("(int32, int64, bool)")
>>> p.match(c)
False
Symbolic dimensions¶
Recall that array types include the dimension kind, which can be symbolic.
Simple symbolic match¶
This matches a concrete fixed size array against the set of all one-dimensional fixed size arrays:
>>> p = ndt("N * float64")
>>> c = ndt("100 * float64")
>>> p.match(c)
True
Symbolic+Dtypevar¶
Symbolic dimensions can be used in conjunction with dtype variables:
>>> p = ndt("N * T")
>>> c = ndt("10 * float32")
>>> p.match(c)
True
Ellipsis match¶
Finally, all dimension kinds (including multiple dimensions) match against ellipsis dimensions (named or unnamed):
>>> p = ndt("... * float64")
>>> c = ndt("10 * 2 * float64")
>>> p.match(c)
True
>>> p = ndt("Dim... * float64")
>>> c = ndt("10 * 20 * float64")
>>> p.match(c)
True
This is used in broadcasting [2].
[1] | An argument is often made that the term dependent types should be reserved for static type systems. We use it here while explicitly acknowledging that the datashape implementation is dynamically typed. |
[2] | (1, 2) Additional section needed. |