Hello! Welcome back to our course on programming theory.
In our last two lessons, we built a solid foundation by defining algebraic data types (ADTs)—first product types (records, tuples) and then sum types (variants, tagged unions). We saw that ADTs are a powerful way to model data. Today, we'll explore the mechanism that makes them so expressive and safe to use: pattern matching.
This lesson directly addresses the learning outcome: Use pattern matching for control flow and data extraction, comparing ML-style matching with Python's recent structural pattern matching.
We will dissect pattern matching into its two core functions: checking the shape of data and pulling values out of it. We'll start with the classic, type-safe style from the ML language family, and then dive into Python's powerful new match statement, analyzing how it adapts these functional ideas to a dynamic, object-oriented world.
1. The Essence of Pattern Matching: ML-Style
At its heart, pattern matching is a sophisticated conditional construct that combines two actions:
- Control Flow: It checks a value against a series of "patterns" to see which one describes its structure. For a list, the patterns might be "is it empty?" or "does it have a head and a tail?". For a custom
Treetype, it might be "is it anEmptytree, aLeaf, or an internalNode?". - Data Extraction: If a pattern matches, it simultaneously "de-structures" the value, binding its constituent parts to new variables that can be used in the corresponding block of code.
This combination of branching on structure and binding variables makes for incredibly clear and robust code, especially for recursive data structures. The ML language family (SML, OCaml, F#) and its descendants (Haskell, Scala) are the exemplars of this style.
Let's start by seeing how pattern matching works on lists, one of the most fundamental recursive data types.
Pattern Matching with Lists | OCaml Programming | Chapter 3 Video 8
This video from a course on OCaml programming by Michael Ryan Clarkson provides a clear, concise demonstration of pattern matching on lists.
Please watch from the beginning to 04:27. Focus on these key ideas: The Two List Patterns (0:00 - 0:25): A list is either empty ([] or nil) or it's an element cons-ed onto another list (h::t). This is the structural basis for the match. Matching for Control Flow (0:25 - 2:30): Observe how match is used to check if a list is empty, replacing a series of if statements. Matching for Data Extraction (2:30 - 4:27): In the sum function, notice how the pattern h::t not only identifies a non-empty list but also extracts the head h and tail t for use in the recursive call.
The sum function is a canonical example of structural recursion, where the recursion follows the structure of the data type. Pattern matching is the natural syntax for expressing this, providing a case for each variant of the data type (the base case [] and the recursive case h::t).
Now, let's see how this applies to the custom ADTs we defined in previous lessons. Your reference text, Concepts in Programming Languages, uses Scala to illustrate this. Scala's case class is explicitly designed to bring ML-style ADTs and pattern matching into an object-oriented context.
Concepts in Programming Languages Practicalities Main ...
These slides from John C. Mitchell's course, based on his book, show how to build and deconstruct an expression tree using case classes and pattern matching in Scala.
Please review slides 227 to 229. Slide 227-228: Briefly review the case class definitions for Expr. Note the key statement: 'Case classes are essentially ML data types in an object-oriented language.' Slide 229: Focus on the eval function. See how the match expression provides a separate case for each variant of the Expr sum type (Number, Sum, Prod). This is a perfect illustration of using pattern matching for both control flow and data extraction.
The eval function is a classic use case. Without pattern matching, you would need a chain of if (e instanceof Number) checks and manual type casts, which is verbose and error-prone. Pattern matching makes the logic clean, safe, and declarative. A key feature in ML-family languages is exhaustiveness checking: the compiler will warn you if you forget to handle a case (e.g., if you added a Divide variant but forgot to update eval), preventing runtime errors.
2. Python's Structural Pattern Matching
Inspired by the power and clarity of ML-style matching, Python 3.10 introduced the match statement. It adapts these concepts to Python's dynamic, object-oriented nature. While it lacks the compile-time exhaustiveness checks of ML, it provides a similarly expressive tool for working with complex data structures.
Let's get a practical overview of its features.
The Hottest New Feature Coming In Python 3.10 - Structural Pattern Matching / Match Statement
This video by mCoding gives an excellent tour of Python's structural pattern matching, covering the most important features with clear, side-by-side comparisons to the 'old way' of writing the code.
Please watch the following segments: Sequence Patterns (0:00 - 6:21): This is the equivalent of list matching in OCaml. Pay attention to how you can match fixed-length sequences, use * for variable-length parts (like OCaml's t in h::t), and use _ as a wildcard. Guards (7:59 - 10:10): A guard is an if clause added to a case that provides an extra condition for a match. This is a feature also present in many ML-family languages. Class Patterns (10:10 - 13:20): This is the most important part for our comparison. This is Python's way of matching on object structure, analogous to matching on ADT constructors like Sum(l, r) in Scala. Notice how you can match on the class type and extract attribute values.
As you can see, Python's match statement is a versatile tool. It can match on:
- Literals:
case 42:,case "hello": - Sequences:
case [x, y]:,case [a, *rest]: - Mappings (Dictionaries):
case {"status": 200, "data": d}: - Class Instances:
case Point(x=x_val, y=y_val):
This brings a declarative, functional style directly into Python.
3. A Deeper Dive: Design and Comparison
To truly understand the comparison between ML-style and Python-style matching, it's valuable to look at the design principles behind Python's implementation. The feature was proposed and documented in a paper co-authored by Guido van Rossum.
Dynamic Pattern Matching with Python - Guido van Rossum
This paper, 'Dynamic Pattern Matching with Python', explains the motivation and design of the match statement. It's a great resource for understanding the 'why' behind the feature.
Please read the following sections to understand the core design principles: Section 2, 'Overview': This sets the stage, explaining the two main goals: extending iterable unpacking and selecting a processing strategy based on data structure. Section 3.1, 'Objectives': This formally states the two objectives of any pattern match: validating the structure and binding variables. This is a universal definition that applies to both ML and Python. Section 4.7, 'Constructors': This is the most critical section. It discusses the challenge of adapting constructor patterns from statically-typed ADTs to Python's dynamic object model. Focus on how Python uses class types and attribute checking, and the role of __match_args__ to map positional patterns to attributes.
Now, let's synthesize what we've learned into a direct comparison.
| Feature | ML-Style (e.g., OCaml, Scala, Haskell) | Python-Style |
|---|---|---|
| Data Model | Matches on Algebraic Data Types (sum and product types). The structure is rigid and defined upfront. | Matches on Python's object model: sequences, mappings, and class instances. The structure is dynamic. |
| Type Safety | Static. The compiler verifies that patterns are well-typed and can check for exhaustiveness, warning if a variant of a sum type is not handled. | Dynamic. Type checks (isinstance) happen at runtime. No built-in compile-time exhaustiveness checking. |
| Deconstruction | Deconstructs ADT variants into their constituent parts. case Sum(l, r): | Deconstructs objects by checking their class and accessing attributes. case Sum(left=l, right=r): |
| Wildcard | _ matches anything without binding. | _ matches anything without binding. |
| Variable Binding | case Number(x): binds the contents of Number to x. | case Number(n=x): binds the n attribute's value to x. |
| Guards | Supported. case (x, y) if x > y => ... | Supported. case [x, y] if x > y: |
Key Trade-Offs:
Your experience in designing complex systems at Revolut and now in EdTech highlights the constant tension between flexibility and correctness. Pattern matching is a microcosm of this trade-off.
- ML's Approach (Correctness-first): By tightly coupling pattern matching with statically-defined ADTs, ML-family languages provide powerful guarantees. The compiler becomes a partner in ensuring you've handled every possible state your data can be in. This is invaluable for building robust systems where unexpected states can have serious consequences (e.g., financial settlement).
- Python's Approach (Flexibility-first): Python's structural matching is designed to work with the existing, dynamic object model. You don't need to define special ADTs; you can match on any class or dictionary. This makes it incredibly useful for inspecting complex, heterogeneous data, like JSON from an API or events in a UI framework. The cost is the loss of compile-time safety; correctness is a runtime concern.
Conclusion
In this lesson, we've seen that pattern matching is a powerful control-flow and data extraction tool that originated in functional languages and has now been adopted by mainstream languages like Python.
Key Takeaways:
- Pattern matching serves two purposes: branching on the structure of data and extracting values from it.
- ML-style matching is tied to statically-defined Algebraic Data Types, enabling the compiler to perform exhaustiveness checking for high reliability.
- Python's structural pattern matching adapts these ideas to a dynamic context, matching on the structure of existing objects like sequences, mappings, and class instances.
- The fundamental trade-off is between the static safety of the ML approach and the dynamic flexibility of the Python approach.
Next Lesson Preview:
We now have all the pieces to build a complete, type-safe program in the functional style. In our next lesson, we will put our knowledge of ADTs and pattern matching into practice to tackle a classic problem: modeling and evaluating a simple mathematical expression language. This will solidify how these concepts work together to create programs that are both elegant and correct.
Can't find a good explanation? Sign up and we'll make it for you
Sign up