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-634Awesome Pattern Matching
fastless fast1
good functionalityeven more features
native syntaxdifferent styles2
fixedextensible3
≥ 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-634apm
33
"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.