Comparison of apm and PEP-634 match
January 8, 2022 · View on GitHub
Python 3.10 introduced the
match statement defined by PEP-634
which implements Structural Pattern Matching. match is also gives us an equivalent for the less powerful switch, as
found in many programming languages.
As a general rule of thumb PEP-634 is generally both faster and more readable, as it uses native python syntax. Every
kind of matching that PEP-634 supports is also supported by apm, but apm
has more features than PEP-634 and works on older Python versions.
A very superficial comparison of the two:
| PEP-634 | Awesome Pattern Matching |
|---|---|
| fast | less fast1 |
| good functionality | even more features |
| native syntax | different styles2 |
| fixed | extensible3 |
| ≥ Python 3.10 | ≥ Python 3.7, pypy3.7, pypy3.8 |
1A word on execution speed: While it is the authors firm believe that if you're interested in raw speed
only Python should not be your choice of language, apm
is actually between 50x to 100x slower than native PEP-634 pattern matching. It is still reasonably fast though. For
example the whole test suite runs in 20ms, so we're talking about the difference from a few nano seconds to a hundred
nano seconds.
2Native syntax vs. different styles: Since apm can not alter the grammar of the python language it
has to use existing language constructs. It offers various styles which use (or absue) existing constructs to different
degrees. The author of this library leaves it up to the users of the library to decide which style suits them best, but
you should stick to one.
The in-depth comparison of PEP-636 and apm
also compares different apm styles. Note also that the shape of the patterns matched themselves does not change and
can be shared between the different styles as well. See below for examples.
3Extensibility: The way apm is built automatically makes patterns a
first-class citizen
(unlike patterns in PEP-634). This means that patterns can be stored in variables, dynamically built, shared, and so on.
It also allows apm to be extended. In fact, a lot of the pre-built patterns in apm
could equally well be defined by users of the library
.
Table of Contents generated with DocToc
Similarities and Differences
Both PEP-634 and apm can match all kinds of patterns:
| PEP-634 | apm |
|---|---|
3 | 3 |
"string value" | "string value" |
[action] | ['action' @ _] |
[action, obj] | ['action' @ _, 'obj' @ _] |
["go", direction] | ["go", 'direction' @ _] |
["drop", *objects] | ["drop", 'objects' @ Remaining()] |
_ | _ |
["north"] | ["go", "north"] | OneOf(["north"], ["go", "north"]) |
["get", obj] | ["pick", "up", obj] | ["pick", obj, "up"] | OneOf(["get", 'obj' @ _], ["pick", "up", 'obj' @ _], ["pick", 'obj' @ _, "up"]) |
["go", ("north" | "south" | "east" | "west") as direction] | ["go", 'direction' @ OneOf("north", "south", "east", "west")] |
[a, b, *rest] | ['a' @ _, 'b' @ _, Remaining('rest' @ _)] |
{"foo": a, "bar": b, **rest} | {"foo": 'a' @ _, "bar": 'b' @ _} ** Remainder('rest' @ _) |
{"foo": str(x)} | {"foo": 'x' @ InstanceOf(str)} |
Matching data classes
Both PEP-634 and apm can match dataclasses:
from dataclasses import dataclass
from uuid import UUID
from apm import *
@dataclass
class Record:
key: UUID
title: str
value: float
given = Record(key=UUID("25982462-1118-43FF-B9A3-1A19D46AF8B2"),
title="An object",
value=187.27)
# apm
result = match(given, Record(_, "An object", 'value' @ Between(100, 200)))
print(result.result) # prints 187.27
# pep634
match given:
case Record(_, "An object", value) if 100 <= value <= 200: # matches
print(value) # prints 187.27
The above example also highlights a difference in the general design of apm. While apm does support case
guards (the if 100 <= x <= 200) such checks are better expressed by using existing patterns (Between in this case).
There is also a general Check pattern that takes a lambda and will only match if that yields true on the value being
matched.
Another visible difference is that apm's match function can also be used as an inline-expression and be used to just
extract data. The returned match-result works like a dictionary and also offers .get(key, default=None):
extracted_value = match(given, Record(_, "An object", 'value' @ Between(100, 200))).get('result', 0)
Matching dictionaries
Dictionaries can conceptually matched the same way as with PEP-634, just the syntax is more verbose. The
following apm pattern:
value = {
"Key": "Value"
}
if result := match(value, {
"Key": 'val' @ InstanceOf(str)
}):
print(f"matches '{result.val}'!")
...matches the same way as the following PEP-634 pattern:
value = {
"Key": "Value"
}
match value:
case {"Key": str(val)}:
print(f"matches '{val}'!")
Strict dictionary matches
Just like PEP-634, apm will match a pattern successfully is a subset matches. But apm can also match
dictionaries strictly, which will match only if the dictionary matches completely:
value = {
"Key": "Value",
"Misc": "Stuff",
}
if result := match(value, {
"Key": "Value"
}):
print("This will match.")
if result := match(value, Strict({
"Key": "Value"
})):
print("This will not match so this message will not be printed.")
The same can be achieved using PEP-634 by matching the remainder of the dictionary and a case guard:
value = {
"Key": "Value",
"Misc": "Stuff"
}
match value:
case {"Key": str(val), **remainder} if not remainder:
print("This will now not match (as intended) as the remainder is explicitly checked to be empty.")
While apm also supports case guards (see below) it is not necessary to use any in this case.
Additionally apm has additional features for matching dictionaries, see dictionaries.md.
Case guards
A case in PEP-634 match can have an if attached like that:
match point:
case Point(x, y) if x == y:
...
apm supports case guards too:
case(point) \
.of(Point('x' @ _, 'y' @ _), when=lambda x, y: x == y, then=...) \
.otherwise(None)
However, often there is no need for case guards as it is often more comfortable to use custom patterns or a pattern from the library of pre-defined patterns. For example the following PEP-634 match:
match point:
case Point(x, 0) if 200 <= x <= 300:
...
Can be done without a case guard at all:
if match(point, Point('x' @ Between(200, 300), 0):
...
This illustrates one of the core useage patterns of apm: Custom patterns can be defined
and re-used (like the Between above).
Additionally apm has the guarded feature for checking multiple guards in the same match branch,
see case_guards.md.
In-Depth comparison
An in-depth comparison of the two can be found in this python script that compares
the PEP-636 examples with the different apm
styles: docs/pep634_vs_different_apm_styles.py
. It is like the Rosetta Stone for PEP-634 and apm.