*An animation made by simple addition of*

a Cartesian origin point with

two spinning polar points in a single code loop

a Cartesian origin point with

two spinning polar points in a single code loop

**tl;dr:**This is a joy-of-programming excursion. I have concluded that Structural Pattern Matching introduced in Python 3.10 is a great asset to the language. I like using it in cases of both single and multiple dispatch instead of`functools.singledispatch`for the former and if-chains for the latter. Why? Visual locality of reference when reading the code: the match statement gives me an excellent overview of how different inputs affect the execution of the function. Additionally, I can finesse behavior of different inputs and their interactions more succinctly than with either of the aforementioned techniques. By the way, in case you did not know, I am a heretic.**Goal:**

- I want Cartesian and Polar point objects in both 2D and 3D flavors.
- I want instances of point types to be immutable.
- I want to freely arithmetically mix these two types without ever having to think about their inherent incompatibilities.
- I want iterators that can take a pair of points and produce sequences of steps between them.

Using a `tuple` as a Cartesian or Polar point just doesn't work very
well. In Python, the arithmetic operators (+, -, *, /) don't actually do arithmetic operations on `tuple`.
Instead they act on tuple's traits as a sequence. `__add__` is a concatenation operator, while
`__mul__` is a repetition action. I need tuples to do vector arithmetic instead.

Of course, vector arithmetic is already done properly in `numpy`. Unfortunately, `ndarray` is difficult
to subclass. All the hand waving and somersaults I'd have to go through with `numpy` just seemed like too much
trouble. Maybe the funtionality I'm looking for already exists somewhere in `numpy`, but I'm looking for my own
adventure.

Arithmetic directly with polar coordinates is nasty buisness. Vector arithmetic doesn't directly work with them. Since I don't want to go there, I decided that a Cartesian universe would be the basis for all arithmetic in this package. Any subclass that doesn't use the cartesian universe must provide conversion both to and from cartesian.

I went ahead and started with `tuple` as the base class of my vector class. Because a `tuple` instance
is an immutable object, the `__init__` constructor is executed too late to give the tuple values. Instead of
writing an `__init__` constructor method, the `__new__` method is the right starting point to subclass a
tuple.

This is where I step into the alleged anti-pattern of multiple dispatch. I'm taking the arguments as a sequence
of discrete values and making the `__new__` method act in different ways based on both the number and types of
the input parameters. In otherwords, I'm passing the args tuple directly into the match statement and using its
pattern matching magic as a multiple dispatch system. In the first three cases below, they're selected not only by the
type of the parameter, but the fact that args has a length of 1. The catch-all default case is for all other cases of
length and content of args.

```
class Vector(tuple):
def __new__(cls, *args):
match args:
case [cls() as an_instance_of_cls]:
# match instances of the calling cls or its derivatives
# this is the identity case
return an_instance_of_cls
case [Vector() as an_instance_of_vector]:
# match any instance of the Vector family not directly in line with cls
# explicitly invoke a conversion - maybe cartesian to polar or vice versa
return cls.as_my_type(an_instance_of_vector)
case [Iterable() as an_iterable]:
# match any old iterable like a generator or numpy array
# create a new instance of this cls
return super().__new__(
cls,
tuple(cls._judge_candidate_value(n) for n in an_iterable),
)
case _:
# discrete values were passed, assume they are to be the coordinate
# values of a new instance of this cls
return super().__new__(
cls, tuple(cls._judge_candidate_value(n) for n in args)
)
```

(see this code in situ in the vector.py file in the points main directory)
Had this been written with a decorator based system for single/multiple dispatch, there would be a flock of alternate
definitions for `__new__` and this unmistakably visible heirarchical structure would be hidden within a flat one. One of
the most common complaints in C++, where overloading functions by type (single/multiple dispatch) is common, centers
around the mysteries why the compiler chose to invoke one method over another. It's hard to see the patterns lurking
within the flattened visual structure of the code on a page. Structural Pattern Matching gives me a visual way to
understand how it is going to work at a glance.

Now I have to make the `Vector` class do proper Vector Arithmetic and interoperate with scalars and iterables
in a sensible manner:

```
def _operation(self, the_other, a_dyadic_fn):
# return a new instance with members that result from a_dyadic_fn applied to
# this instance zipped with the_other
match the_other:
case Number() as a_number:
# match scalars
return self.__class__(
*starmap(
a_dyadic_fn, zip_longest(self, (a_number,), fillvalue=a_number)
)
)
case Vector() as a_vector:
# match any instance of the Vector family
the_other_as_my_type = self.as_my_type(a_vector)
return self.__class__(
*starmap(a_dyadic_fn, zip(self, the_other_as_my_type))
)
case Iterable() as an_iterable:
# match any other type of iterable
return self.__class__(*starmap(a_dyadic_fn, zip(self, an_iterable)))
case _:
# no idea how to interpret this value as an operand
raise TypeError(f"{the_other} disallowed")
def __add__(self, the_other):
return self._operation(the_other, add)
# other Vector Arithmetic operations ommited for brevity
```

(see this code in situ in the vector.py file in the points main directory)
The middle case above, `case Vector()`, demonstrates a key class method that subclasses ought to override:
`as_my_type`. Vectors instances themselves impose no rules on how its components are interpreted. The first
layer of subclasses are required to do so. The `CartesianPoint` subclass interprets its vector components in
the realm of cartesian coordinate systems. The `PolarPoint` subclass, appropriately, interprets its components
in a circular/spherical universe.That's where the `as_my_type` class method comes in. Its job is to take a
value and convert it into a value compatible with the calling class' coordinate world view.

Here's how `CartesianPoint` does it:

```
@classmethod
def as_my_type(cls, the_other):
# This function is in charge of converting things into cartesian coordinates.
# The base class Vector has no sense of what its members mean, so members of the
# cartesian branch of the Vector family interpret base Vector instances and other
# Iterables as having cartesian values already.
match the_other:
case cls():
# Match an instance of this cls or its derivatives - identity case
# For example cls may be CartesianPoint but IntPoints would match
# because an IntPoint is a CartesianPoint
return the_other
case Vector():
# match an instance of base class Vector, but of another lineage
# other than CartesianPoint. For example, this ensures that a
# PolarPoint is properly converted to Cartesian.
try:
return the_other.as_cartesian(cls)
except AttributeError:
# Vector itself doesn't know anything about being cartesian, so just
# make a new instance of cls from it
return cls(*the_other)
case Iterable():
# make an instance of cls from any iterable assuming that the
# coordinate values are already cartesian.
return cls(*the_other)
case _:
raise TypeError(
f"No conversion defined for {the_other.__class__} to {cls}"
)
```

(see this code in situ in the cartesian.py file in the
points main directory)
Contrast that with how `PolarPoint` does it:

```
@classmethod
def as_my_type(cls, the_other):
# This function is in charge of converting things into polar coordinates.
# The base class Vector has no sense of what its members mean, so members of the
# polar branch of the Vector family interpret base Vector instances and other
# Iterables as having polar values already.
match the_other:
case cls():
# identity case
return the_other
case CartesianPoint() as a_cartesian_point:
# we know this is the cartesian case, so we must explicitly convert it
return cls.as_polar(a_cartesian_point)
case Iterable() as an_iterator:
# we don't know what this sequence represents.
# To be consistent with the constructor, assume they are
# series of components appropriate for a polar point
return cls(*an_iterator)
case _:
raise TypeError(
f"No conversion defined for {the_other.__class__} to {cls}"
)
```

(see this code in situ in the polar.py file in the points main directory)
Finally, the `PolarPoint` needs to know how to do arithmetic by converting to `CartesianPoints`. That's
accomplished simply by overriding the `Vector` base class arithmetic operators:

```
def __add__(self, the_other):
return PolarPoint(self.as_cartesian() + the_other)
def __sub__(self, the_other):
return PolarPoint(self.as_cartesian() - the_other)
# the rest of the arithmetic operations ommitted for brevity
```

(see this code in situ in the polar.py file in the points main directory)
Taken all together, these point classes work together seamlessly. The code for drawing the loops in the image above
is gorgeously simple in form* (a least until the black code formatter mangled the single loop into
something nearly unreadable - seriously, do we really need to maintain compatibility with punch cards in the 21st
century?)*:

```
def draw_a_looping_spiral(a_canvas):
# the middle of the image
cartesian_middle_point = CartesianPoint(a_canvas.the_image.size) / 2
# beginning and end polar points for two loops around a circle
# while the radius of the loop shrink
larger_rotator_polar_origin = PolarPoint(
cartesian_middle_point * (3.0 / 4.0, 0)
)
larger_rotator_polar_destination = PolarPoint(0, 4.0 * π)
larger_rotator_iter = iter_linearly_between(
larger_rotator_polar_origin, larger_rotator_polar_destination, 2000
)
# beginning and end polar points for fifty loops around a circle
# with the loop radius shrinking with each step
smaller_rotator_polar_origin = PolarPoint(
cartesian_middle_point * (1.0 / 8.0, 0)
)
smaller_rotator_polar_destination = PolarPoint(0, 100.0 * π)
smaller_rotator_iter = iter_linearly_between(
smaller_rotator_polar_origin, smaller_rotator_polar_destination, 2000
)
# create a couple iterators that will produce a sequence of polar points
# that spin in lockstep with each other
for step_counter, (
larger_rotated_polar_point,
smaller_rotated_polar_point,
) in enumerate(
no_consectutive_repeats_iter(
zip(
larger_rotator_iter,
smaller_rotator_iter,
)
)
):
# add the cartesian middle point with the spinning polar points
current_cartesion_point = (
cartesian_middle_point
+ larger_rotated_polar_point
+ smaller_rotated_polar_point
)
# draw the line segment from prevous and current cartesian points
a_canvas.draw_successive_line_segment(current_cartesion_point, step_counter)
```

(see this code in situ in thespirals.py file in the
points demo directory)
This family of classes plays a huge role in the development and production of my artwork. Using these classes, I prove that my mazes are solvable with exactly one path. Further they underlie the zooms, pans and rotations, as well as the moving solution lines in my animation, The Dead Spider Waltz.

Since these classes are built on the base class of tuple, they are appropriate for use *anywhere* a
`tuple` is appropriate. This includes indexing `numpy ndarray` and working with images in the
`pillow` package.

The full working code of from this post is available in my github repo. However, I've not made this an installable package as that would imply that I'm willing to maintain it in perpetuity. Maybe it will inspire someone to make a more serious version that accomplishes the same thing with more more aplomb and efficiency. This implementation may well be full of problems, can't-get-there-from-here situations, or other such troubles. There is no warranty, no guarantee, and the author is absentee.

*Please support my artwork, coding and writing on Patreon.
Even a small conribution helps.*

Yeah, but is Structural Pattern Matching actually
Pythonic?

After deep introspection, I've come to

the conclusion that it just doesn't matter.