Abstract 1 Introduction 2 Motivation 3 The Algebra of Patterns 4 Terms and Types 5 Normalizing Algebraic Patterns 6 Compiling Algebraic Patterns 7 Future Work 8 Related Work 9 Conclusion Data-Availability Statement References

The Algebra of Patterns

David Binder ORCID University of Kent, Canterbury, UK Lean Ermantraut ORCID Radboud University, Nijmegen, The Netherlands
Abstract

Pattern matching is a popular feature in functional, imperative and object-oriented programming languages. Language designers should therefore invest effort in a good design for pattern matching. Most languages choose a first-match semantics for pattern matching; that is, clauses are tried in the order in which they appear in the program until the first one matches. As a consequence, the order in which the clauses appear cannot be arbitrarily changed, which results in a less declarative programming model. The declarative alternative to this is an order-independent semantics for pattern matching, which is not implemented in most programming languages since it requires more verbose patterns. The reason for this verbosity is that the syntax of patterns is usually not expressive enough to express the complement of a pattern. In this paper, we show a principled way to make order-independent pattern matching practical. Our solution consists of two parts: First, we introduce a boolean algebra of patterns which can express the complement of a pattern. Second, we introduce default clauses to pattern matches. These default clauses capture the essential idea of a fallthrough case without sacrificing the property of order-independence.

Keywords and phrases:
functional programming, pattern matching, algebraic data types, equational reasoning
Funding:
David Binder: supported by ARIA programme for Safeguarded AI (grant MSAI-PR01-P11).
Copyright and License:
[Uncaptioned image] © David Binder and Lean Ermantraut; licensed under Creative Commons License CC-BY 4.0
2012 ACM Subject Classification:
Theory of computation Lambda calculus
; Theory of computation Type theory
Related Version:
Extended Version: https://arxiv.org/abs/2504.18920 [2]
Supplementary Material:
Software  (ECOOP 2025 Artifact Evaluation approved artifact): https://doi.org/10.4230/DARTS.11.2.10
Editors:
Jonathan Aldrich and Alexandra Silva

1 Introduction

Pattern matching is a programming language feature that combines two operations: Testing whether a value matches a given pattern and, if the test is successful, extracting subexpressions of the value and binding them to variables. A pattern matching expression consists of the scrutinee which is analyzed by the pattern match and a list of clauses. Each clause consists of a pattern, hence the name, and an expression called the right-hand side. These elements can all be seen in the following example of the length function, which computes the length of a list:

Programming with pattern matching is popular. For a long time, only programmers who use functional languages could enjoy this feature, but today pattern matching has arrived in the mainstream of object-oriented and imperative programming languages. For example, Python, Java, Dart, and C# added pattern matching as an extension to the language, and some newer languages such as Rust and Swift, which are not primarily functional languages, supported pattern matching from the beginning.

Pattern matching has become one of the central constructs that is used to structure the control and data flow of a program. How pattern matches are checked and evaluated therefore fundamentally determines how we reason about the meaning of our programs; both informally when we read and write code in our daily lives, and formally in proof assistants, static analyzers and other verification tools. It is therefore important to choose a semantics for pattern matching which makes reasoning as simple as possible. One choice that language designers have to make is whether the clauses are tried in the order in which they appear in the program, or whether the order of the clauses does not matter. The following definition captures this difference:

Definition 1 (First-Match and Order-Independent Semantics).

A language uses first-match semantics if the clauses of a pattern match are tried in the order in which they occur until the first pattern matches the scrutinee. A language uses order-independent semantics if any of the matching clauses in a pattern match can be used to non-deterministically reduce a pattern matching expression.

Overlapping clauses, i.e. two clauses with patterns that can match the same scrutinee, are a problem for order-independent semantics since a pattern-matching expression with overlapping clauses can non-deterministically reduce to two different results. The obvious solution is to enforce that two clauses must never overlap. But if we adopt this solution, then programmers usually have to write much more verbose patterns. For example, instead of the following function which checks whether the argument is the color 𝚁𝚎𝚍

isRed(c)𝐜𝐚𝐬𝐞c𝐨𝐟{𝚁𝚎𝚍𝚃𝚛𝚞𝚎,_𝙵𝚊𝚕𝚜𝚎},

the programmer has to write the following more verbose expression

isRed(c)𝐜𝐚𝐬𝐞c𝐨𝐟{𝚁𝚎𝚍𝚃𝚛𝚞𝚎,𝙶𝚛𝚎𝚎𝚗𝙵𝚊𝚕𝚜𝚎,𝙱𝚕𝚞𝚎𝙵𝚊𝚕𝚜𝚎}.

The source of this verbosity is the poor expressiveness of patterns, which usually do not allow to express the complement of a pattern without exhaustively enumerating all the alternatives. In this example, we had to explicitly mention both 𝙱𝚕𝚞𝚎 and 𝙶𝚛𝚎𝚎𝚗 to express the complement of the pattern 𝚁𝚎𝚍. In languages that support or-patterns we can combine the last two clauses in the single clause (𝙶𝚛𝚎𝚎𝚗𝙱𝚕𝚞𝚎)𝙵𝚊𝚕𝚜𝚎, but we still have to mention all constructors explicitly. It is unreasonable to ask programmers to always list all constructors of a data type, and they would rightfully reject a programming language which enforces this. Every proposal to use order-independent pattern matching in a realistic programming language therefore has to address this verbosity problem.

We propose two complementary solutions to the verbosity problem. In our system, the function can be written in the following ways:

isRed(c) 𝐜𝐚𝐬𝐞c𝐨𝐟{𝚁𝚎𝚍𝚃𝚛𝚞𝚎,¬𝚁𝚎𝚍𝙵𝚊𝚕𝚜𝚎} (Negation Pattern)
isRed(c) 𝐜𝐚𝐬𝐞c𝐨𝐟{𝚁𝚎𝚍𝚃𝚛𝚞𝚎,𝐝𝐞𝐟𝐚𝐮𝐥𝐭𝙵𝚊𝚕𝚜𝚎} (Default Clause)

To write the first variant we enrich the syntax of patterns with negation patterns which match whenever the negated pattern does not match. The two patterns are no longer overlapping, and at the same time, the pattern matching expression is not more verbose than our first version. Such negation patterns were already proposed under the name of “anti-patterns” [16, 17, 6] and are also mentioned by some other authors [19, 11]; the core contribution of this paper, however, is to consider them in the context of a complete boolean algebra of patterns which allows us to apply algebraic reasoning to patterns.

To write the second variant we enrich the syntax of pattern-matching expressions with default clauses111Some programming languages support default clauses with similar semantics to what we propose. Ada, for example, enforces that clauses in a case statement are not overlapping, but allows an optional “other” clause with the same semantics as our default clause. The clauses that Ada allows are however much more restricted, since they only work for enums instead of algebraic data types. . A pattern matching expression can contain at most one such default clause (where 𝐝𝐞𝐟𝐚𝐮𝐥𝐭 is a keyword instead of a pattern) which matches whenever none of the normal clauses matches the scrutinee. At first glance, the meaning of a default clause looks identical to the meaning of a clause with a wildcard pattern, but this is only the case for first-match semantics. If we use order-independent semantics, then we see that the meaning of a default clause does not correspond to a clause with a wildcard pattern, but to a clause that is headed by the negation of the disjunction of all other patterns in the pattern matching expression. In the compilation algorithm, however, default clauses are treated natively, so that we don’t have to expand a default clause to a large pattern in the case of pattern matches with many clauses.

We are particularly interested in the kind of reasoning which is preserved when we add or remove clauses from a pattern match, or when we add or remove constructors from a data type. We will argue that this is a significant software engineering challenge that most pattern-based languages do not adequately address yet.

1.1 Algebraic Reasoning and Variable Binding

The main contribution of this work is to support a rich catalog of equivalences for patterns. In fact, we strive to treat patterns as a boolean algebra which consists of negation patterns, and-patterns, or-patterns, wildcard-patterns and absurd-patterns, featuring all the expected equivalences. This is challenging from the perspective of variable binding. For instance, we want the law of double negation, i.e. ¬¬pp, to hold, but this entails that we cannot ignore variables that occur inside patterns in a negated context. We will demonstrate that we can achieve the desired equivalences by a variable binding definition that tracks whether variables occur under an even or odd number of negations. A related problem is to have well-defined variable binding in the context of or-patterns pp, which requires a careful definition of linearity to make sure that the same variables are bound in all cases of an or-pattern. We also describe the conditions under which we can replace a pattern p1p2 by the pattern p2p1; this transformation is not semantic preserving in most systems which implement or-patterns.

1.2 Overview and Contributions

The rest of this article is structured as follows:

  • In Section 2 we motivate why the first-match semantics that most programming languages use for pattern matching fails to address several important reasoning and software engineering problems. We show how our contributions can improve upon this status quo.

  • In Section 3 we present our first main contribution: the algebra of patterns. We describe the syntax of patterns, the rules for non-deterministically matching a pattern against a value, and the restrictions that characterize the subset of linear and deterministic patterns. We also prove the algebraic properties that characterize the semantic equivalence relation on patterns.

  • Section 4 introduces our second contribution, a small term language that contains pattern-matching expressions with default clauses. This section also introduces exemplary typing rules to the untyped patterns of Section 3 and the constructs of our term language; the compilation of patterns described in later sections, however, does not depend on patterns being typed.

  • Not all extensions to pattern matching can be compiled to efficient code, but ours can. In Section 5 we show how to compile patterns by translating them to a variant of a disjunctive normal form. We use these normalized patterns in the compilation of pattern-matching expressions to decision trees, which we describe in Section 6.

  • We discuss future work in Section 7, related work in Section 8 and conclude in Section 9.

The results in sections Section 3 have been formally verified in the proof assistant Rocq, and are made available as complementary material. Theorems and definitions which have been formalized are marked with the symbol [Uncaptioned image].

2 Motivation

Most programming languages that implement pattern matching use a first-match semantics for which the order of clauses matters. Changing this state of affairs is our main motivation for introducing a more expressive set of algebraic patterns. Let us therefore explain why we think that first-match semantics is not a good default choice for declarative languages.

2.1 First-Match Semantics Weakens Equational Reasoning

Equational reasoning is one of the most important methods functional programmers use to reason about code. Equational reasoning allows us to show that two expressions are equivalent if we can obtain one from the other by a series of rewriting steps. Each rewriting step is justified by some equation between terms, and functions defined by pattern matching are one of the main sources of such equations. Take, for example, the definitions of the functions “id” and “map”:

id(x)x (1)
map(f,[])[] (2)
map(f,x::xs)f(x)::map(f,xs) (3)

We can show that mapping the identity function over a list returns the original list, i.e. that map(id,xs)=xs holds, by using the individual clauses of the definition of “map”:

map(id,[]) =(2)[]
map(id,x::xs) =(3)id(x)::map(id,xs)=(1)x::map(id,xs)=(IH)x::xs

We have annotated each rewriting step with the equation that justifies it, and in the last step, we have used the induction hypothesis (IH). In this example, we have used the fact that every clause of the pattern match is a valid equation that we can use for rewriting. But this was only valid because the equations in the definition of “map” do not overlap. To see why this is essential, consider the example from the introduction again:

isRed(𝚁𝚎𝚍)𝚃𝚛𝚞𝚎 (4)
isRed(_)𝙵𝚊𝚕𝚜𝚎 (5)

If we are using these clauses as equations we can now show that True is equal to False, using the proof 𝚃𝚛𝚞𝚎=(4)isRed(𝚁𝚎𝚍)=(5)𝙵𝚊𝚕𝚜𝚎. A human is of course very unlikely to make such a silly mistake, but if we cannot guarantee that every clause holds as a valid equality between expressions, then we also limit what automatic tools can do for us.

2.2 First-Match Semantics Complicates Reasoning About Change

Software engineering has been described as the problem of integrating programming over time [27]. This means that we not only have to understand the meaning of a program at a fixed point in time, but we also have to understand how the meaning of a program changes as it is developed and maintained. It should therefore be as simple as possible to reason about how the meaning of a program changes when a programmer adds a clause to a pattern match, removes a clause from a pattern match, adds a constructor to a data type, or removes a constructor from a data type. Let us see why first-match semantics complicates reasoning about these kinds of changes.

If we want to understand the consequences of adding or removing a clause from a pattern match, then we have to consider all clauses occurring both above and below the clause we are changing. Having to reason about the entire context of the clause makes it much harder to spot bugs in code reviews, since it is not uncommon that pattern matching expressions can span multiple pages of code. Order-independent semantics guarantees that it doesn’t matter if we add, delete or modify a clause at the beginning, middle or end of a list of clauses.

The other kind of change that frequently occurs is that we add or remove a constructor from an existing data type. Whether we have to adjust existing pattern matches depends on how those have been written. Even if we enforce that clauses in a pattern matching expression must not overlap, we have multiple possibilities to write exhaustive pattern matches. Consider a data type “Group” which consists of administrators, registered users and guests. We want to guarantee that only administrators have write access, but we have two possibilities to write the function:

hasWriteAccess:GroupBoolhasWriteAccess:GroupBoolhasWriteAccess(𝙰𝚍𝚖𝚒𝚗)𝚃𝚛𝚞𝚎hasWriteAccess(𝙰𝚍𝚖𝚒𝚗)𝚃𝚛𝚞𝚎hasWriteAccess(𝚁𝚎𝚐𝚒𝚜𝚝𝚎𝚛𝚎𝚍𝚄𝚜𝚎𝚛𝙶𝚞𝚎𝚜𝚝)𝙵𝚊𝚕𝚜𝚎hasWriteAccess(¬𝙰𝚍𝚖𝚒𝚗)𝙵𝚊𝚕𝚜𝚎

On the left side we have pattern matched exhaustively on the complement of Admin, whereas on the right side we have used a negation pattern. These two programs behave the same, but we can observe a difference when we add moderators as a fourth type of user. We have to revisit the function on the left, whereas the function on the right continues to compile.

Some programming languages have already started to add support for enforcing these kind of considerations. For example, Rust supports the #[non_exhaustive] attribute on type declarations222Cp. doc.rust-lang.org/stable/reference/attributes/type_system.html.. This attribute restricts pattern matches and ensures that they continue to compile after a new constructor has been added to the type; annotating the Group type with such an attribute would disallow the definition on the left. Dually, OCaml warns against writing “fragile pattern matches”. A fragile pattern can hide newly added constructors, which might have unintended consequences. The algebraic patterns presented in this paper provide the necessary tools to program in situations where the programmer wants to enforce either one of these dual restrictions. There is also some empirical evidence that programmers feel uneasy about using wildcard patterns in evolving codebases. For example, the authors of a recent study about the programming practices of functional programmers [21] write: “Participants felt similarly about the use of wildcards in pattern matching, which silently assign pre-existing behavior to new variants of an enumeration. P7 mentioned that, in their main codebase, wildcards are completely disallowed for this reason, even though they make programming more convenient.”

2.3 Compositional Pattern Matching and Extensible Data Types

Our main motivation in this paper is to obtain a more declarative programming model for standard nominal data types and ordinary pattern matching. The compilation algorithm that we are presenting, however, also works for polymorphic variants [10]. Polymorphic variants are used in the programming language OCaml, for example, to handle open sets of possible error conditions without having to declare a data type which lists all possible errors. Our algorithm does not presuppose a data type declaration with a fixed set of constructors which is necessary to check for exhaustiveness; we do not discuss exhaustiveness of patterns in the main part of this paper, but outline the necessary modifications to check for it in the extended version [2]. We think that the ideas and the algebraic patterns that we present in this paper are also useful for some proposed solutions of the expression problem. In the compositional programming approach [25, 29, 28], for example, we can create pattern matches whose clauses are distributed among multiple different modules. In that scenario it is especially important to guarantee that the contributing clauses which come from different modules do not overlap in their left-hand side.

3 The Algebra of Patterns

As our first contribution, we introduce an algebra of patterns. The syntax of patterns and values is specified in Figure 1. We assume that we have an infinite set CtorNames of constructor names such as 𝚃𝚛𝚞𝚎, 𝙵𝚊𝚕𝚜𝚎 or 𝙽𝚒𝚕, and a set Variables which contains variables such as x,y or z. Each constructor 𝒞 comes with an implicit arity n, and we sometimes write 𝒞n to make this explicit. For example, the constructor 𝙲𝚘𝚗𝚜 has arity 2 since it needs to be applied to two arguments. Patterns p consist of variable patterns x, constructor patterns 𝒞n(p1,,pn), and-patterns p&p, or-patterns pp, wildcard-patterns _, absurd-patterns #, and negation patterns ¬p. Values consist of constructors applied to other values; the two-element list 𝙲𝚘𝚗𝚜(𝚃𝚛𝚞𝚎,𝙲𝚘𝚗𝚜(𝙵𝚊𝚕𝚜𝚎,𝙽𝚒𝚕)) is an example of a value. If a pattern p matches a value v we obtain a substitution σ. Such substitutions are formalized as lists of mappings xv, and we write σ1++σ2 for the concatenation of two such lists. Not every list of mappings is a proper map since some lists contain multiple mappings for the same variable, but we will later prove that patterns from the subset of linear patterns produce substitutions that map a variable to at most one value. There are two functions that compute the free variables contained in a pattern. The function FVe(p) computes the free variables that occur under an even number of negations, whereas FVo(p) computes the variables under an odd number of negations.

𝒞,𝒞nCtorNamesx,y,zVariablespx𝒞n(p1,,pn)p&ppp_#¬pPatternsv𝒞n(v1,,vn)ValuesmxvMappingσ[]m::σSubstitution
FVe(x){x}FVo(x)FVe(_)FVo(_)FVe(#)FVo(#)FVe(¬p)FVo(p)FVo(¬p)FVe(p)FVe(p1p2)FVe(p1)FVe(p2)FVo(p1p2)FVo(p1)FVo(p2)FVe(p1&p2)FVe(p1)FVe(p2)FVo(p1&p2)FVo(p1)FVo(p2)FVe(𝒞(p1,,pn))i=1nFVe(pi)FVo(𝒞(p1,,pn))i=1nFVo(pi)
Figure 1: The syntax of values, patterns and substitutions. We track whether variables occur under an even or odd number of negations.

Variable, constructor and wildcard patterns are standard, so we do not introduce them explicitly and turn directly to the more interesting absurd-patterns, or-patterns, negation-patterns, and and-patterns.

The absurd-pattern # is the dual of the wildcard-pattern _. Whereas the wildcard-pattern matches any value, the absurd-pattern never matches a value. We include the absurd-pattern in the syntax of patterns since we need a canonical bottom element of the boolean algebra. But there are also more practical reasons to include an absurd pattern. In the lazy programming language Haskell, for example, we sometimes have to write a pattern that never matches but forces the evaluation of an argument. This is often achieved by adding a guard clause that evaluates to false, but we can also use an absurd pattern for the same purpose.

An or-pattern p1p2 matches a value if either p1 or p2 (or both) matches the value. Or-patterns introduce a complication for the compiler: we have to ensure that if values are bound to variables in p1 during matching, then p2 has to bind values of the same type to the same variables. The following example on the left shows how or-patterns without variables can be used to combine multiple clauses into one.

𝐝𝐚𝐭𝐚Day=𝙼𝚘𝚃𝚞𝚆𝚎𝚃𝚑𝙵𝚛𝚂𝚊𝚂𝚞isWeekend:DayBoolisWeekend:DayBoolisWeekend(𝚂𝚊𝚂𝚞)𝚃𝚛𝚞𝚎isWeekend(𝚂𝚊𝚂𝚞)𝚃𝚛𝚞𝚎isWeekend(𝙼𝚘𝚃𝚞𝚆𝚎𝚃𝚑𝙵𝚛)𝙵𝚊𝚕𝚜𝚎isWeekend(¬(𝚂𝚊𝚂𝚞))𝙵𝚊𝚕𝚜𝚎

We could also have written the second clause of the previous example using a wildcard pattern, but then the two patterns would overlap. We can get rid of the verbose second clause by using a negation pattern, which is illustrated on the right. A negation pattern matches if, and only if, the negated pattern does not match.

The last type of patterns we introduce are and-patterns p1&p2 which succeed if both p1 and p2 match the scrutinee. While or-patterns p1p2 require that p1 and p2 bind the same set of variables, and-patterns p1&p2 require p1 and p2 to bind disjoint sets of variables. Many languages already have a special case of and-patterns: An @-pattern x@p (some languages also use the keyword “as”) is an and-pattern where the left side must be a variable. If we want to rewrite the previous example to return a string instead of a boolean value, then we could use the following and-patterns:

isWeekend:DayString
isWeekend(x&(𝚂𝚊𝚂𝚞))show(x)++“ is on the weekend”
isWeekend(x&¬(𝚂𝚊𝚂𝚞))show(x)++“ is not on the weekend”

Here we use the show function to print a weekday and ++ for string concatenation. We will use this snippet as a running example in Section 5 and Section 6 to show how patterns are normalized and compiled to efficient code.

3.1 Matching Semantics of Patterns

We will now present the formal rules which specify what happens when we match a pattern against a value; these rules can be found in the upper half of Figure 2. We use two mutually recursive judgments to formalize the semantics of matching: The judgment pvσ expresses that the pattern p successfully matches the value v and also binds values to variables of the pattern; those mappings are recorded in the substitution σ. The judgment pvσ expresses that the pattern p does not match the value v, and also produces a substitution. That the judgment pvσ also produces a substitution might be surprising: How can there be a substitution if the value does not match the pattern? We can think of these as “temporarily deactivated” substitutions which will become available again if we enclose the pattern in an additional negation. In fact, such substitutions are necessary if we want to guarantee that the pattern p is semantically equivalent to ¬¬p in the case where p contains variables, for example in Equation 5 below.

Figure 2: Rules for matching patterns against values, and checking for linearity and determinism.
Example 2.

These examples show some well-behaved instances of the matching judgments:

Cons(x,xs)Cons(2,Cons(3,Nil))[x2,xsCons(3,Nil)] (1)
TrueFalseTrue[] (2)
TrueFalse[] (3)
¬xTrue[xTrue] (4)
¬¬xTrue[xTrue] (5)

Equation 1 illustrates how the rules Var and Ctor can be used to match a constructor pattern against a list while binding components of the list to variables x and xs. Equation 2 shows how the rule Or1 (together with the rule Ctor) allows to successfully match the left side of an or-pattern against a value. Equation 3 illustrates the rule Ctor2 and Equation 4 the combination of rules Var and Neg2. Equation 5 shows why we need two mutually recursive judgments: in order for the pattern ¬¬x to match against the value True we need to use the rules Neg1, Neg2 and Var to ensure that the variable x is bound to the value True.

Using the rules for pvσ and pvσ we can already prove that they are, in a certain sense, sound and complete:

Theorem 3 (Matching rules are sound, [Uncaptioned image]).

The rules from Figure 2 are sound. There is no pattern p, value v and substitutions σ1,σ2 such that both pvσ1 and pvσ2 hold.

Theorem 4 (Matching rules are complete, [Uncaptioned image]).

The rules from Figure 2 are complete, i.e. for any pattern p and value v there is some σ such that pvσ or pvσ holds.

Since we are interested in algebraic reasoning, we have to say when we consider two patterns to be semantically equivalent. We will define semantic equivalence in Definition 6 using the two judgments introduced above. But first, we have to define when two substitutions (which we formalized as lists of mappings) are semantically equivalent:

Definition 5 (Semantic equivalence of substitutions, [Uncaptioned image]).

Two substitutions σ and σ are semantically equivalent if they contain the same mappings:

σσm,mσmσ

We can now state the definition for semantic equivalence of patterns.

Definition 6 (Semantic equivalence of patterns, [Uncaptioned image]).

Two patterns p and q are semantically equivalent if they match the same values with equivalent substitutions, and if they also do not match the same values with equivalent substitutions:

pqv,σ, pvσσ,qvσσσ
qvσσ,pvσσσ
pvσσ,qvσσσ
qvσσ,pvσσσ

We have to include both judgment forms if we want semantic equivalence to be a congruence relation. To see why this is the case, suppose that we omit the second half of Definition 6. It would then follow that the two patterns # and ¬x are semantically equivalent since they match against the same set of values (i.e. no values at all). But if we apply a negation-pattern to both patterns, then we can easily show that the resulting patterns are no longer semantically equivalent: ¬#_x¬¬x. But since Definition 6 requires both patterns to agree on the values they match and don’t match against, it actually defines a congruence relation on patterns:

Theorem 7 (Congruence, [Uncaptioned image]).

If p1p1 to pnpn hold, then we also have ¬p1¬p1, p1&p2p1&p2, p1p2p1p2 and 𝒞(p1,,pn)𝒞(p1,,pn).

We can use the definition of semantic equivalence to prove that the following algebraic laws hold for patterns:

Theorem 8 (Algebraic Equivalences of Patterns, I, [Uncaptioned image]).

For all patterns p,q,r, the following equivalences hold:

p&qq&ppqqpCommutativityp&(q&r)(p&q)&rp(qr)(pq)rAssociativityp&_pp#pNeutral Elements¬_#¬#_Duality¬(pq)(¬p)&(¬q)¬(p&q)(¬p)(¬q)De Morgan¬¬ppDouble Negation

In addition to these boolean laws we can also prove the following equivalences which involve constructor patterns:

Theorem 9 (Equivalences of Constructor Patterns, I, [Uncaptioned image]).

For all patterns p1 to pn and p1 to pn, the following equivalences hold:

𝒞(p1,,pn)&𝒞(p1,,pn) 𝒞(p1&p1,,pn&pn)
𝒞(p1,,pipi,,pn) 𝒞(p1,,pi,,pn)𝒞(p1,,pi,,pn)

There are still some equivalences that we expect to hold but which are missing from Theorem 8 and Theorem 9. These additional laws, like the distributive law for and- and or-patterns, are not universally valid and require additional restrictions on patterns. We will motivate and introduce these additional constraints in the next subsection.

3.2 Linear Patterns

The first restriction we impose on patterns is linearity. The linearity restriction is easy to state if the syntax of patterns does not include and-patterns, or-patterns and negation-patterns: A pattern is linear if every variable occurs at most once. To see why this restriction is necessary consider the nonlinear pattern 𝙲𝚘𝚗𝚜(x,x), for which we can derive the following judgment:

𝙲𝚘𝚗𝚜(x,x)𝙲𝚘𝚗𝚜(2,𝙽𝚒𝚕)[x2,x𝙽𝚒𝚕] (6)

The problem here is that multiple values are bound to the same variable, i.e. the result of the pattern match is not a proper substitution.

We have a more expressive language of algebraic patterns, and the simple definition of linearity described above is no longer sufficient. To see why this is the case, consider the pattern (x&𝚃𝚛𝚞𝚎)𝙵𝚊𝚕𝚜𝚎 that is linear according to the simple definition, but which shows the following pathological behavior:

(x&𝚃𝚛𝚞𝚎)𝙵𝚊𝚕𝚜𝚎𝚃𝚛𝚞𝚎[x𝚃𝚛𝚞𝚎] (7)
(x&𝚃𝚛𝚞𝚎)𝙵𝚊𝚕𝚜𝚎𝙵𝚊𝚕𝚜𝚎[] (8)

Equations 7 and 8 show that matching against this pattern can produce substitutions that do not have the same domain.

The two examples above tell us what the correct notion of linearity should guarantee: First, if a linear pattern matches against a value, then the resulting substitution should be a proper substitution which contains at most one mapping for a variable. Second, if we use a pattern in a clause we should be able to read off the variables that will be in the domain of the substitution of a successful match.

In order to ensure that these properties hold we have to use two mutually recursive judgments p𝐥𝐢𝐧+ and p𝐥𝐢𝐧, whose rules are given in Figure 2. We use p𝐥𝐢𝐧± for patterns which satisfy both p𝐥𝐢𝐧+ and p𝐥𝐢𝐧. Theorems 10 and 11 show that linear patterns do have the properties described in the previous paragraph.

Theorem 10 (Linear patterns produce covering substitutions, [Uncaptioned image]).

For any pattern p, value v and substitution σ:

  1. 1.

    If p𝐥𝐢𝐧+ and pvσ, then the domain of σ is equal to FVe(p).

  2. 2.

    If p𝐥𝐢𝐧 and pvσ, then the domain of σ is equal to FVo(p).

Theorem 11 (Linear patterns produce proper substitutions, [Uncaptioned image]).

For any pattern p, value v and substitution σ:

  1. 1.

    If p𝐥𝐢𝐧+ and pvσ, then σ is a proper substitution.

  2. 2.

    If p𝐥𝐢𝐧 and pvσ, then σ is a proper substitution.

A substitution σ is proper if any variable occurs at most once in the domain of σ.

We will now motivate why the rules in Figure 2 have the specific form that they do. Theorem 10 tells us that we should think about the variables in FVe(p) as the actual pattern variables which we can use in the right-hand side of a clause, and that we should think of the variables in FVo(p) as the “temporarily deactivated” pattern variables “reactivated” by negation.

Let us first look at the restrictions on the variables in FVe(p), since those are more intuitive. In the or-pattern p1p2 we require that FVe(p1) is equal to FVe(p2) since we do not know which of the two patterns will match, and we have to ensure that the same variables appear in the substitution. In the and-pattern p1&p2 we require that FVe(p1) is disjoint from FVe(p2) since we do not want the same variable to be mapped to two values in the substitution. The reasoning is very similar for constructor patterns 𝒞(p1,,pi) where we require all the FVe(pi) to be disjoint.

Next, let us look at the restrictions on FVo(e), i.e. the variables which occur under an odd number of negations. The rules for and-patterns and or-patterns are motivated by duality. We have seen that the De Morgan rules are valid for patterns. We therefore want, for example, that if the pattern ¬(p1p2) is linear, the pattern ¬p1&¬p2 should be linear as well. For this to be true, the restrictions on FVo(p) for or-patterns have to mirror the restrictions on FVe(p) for and-patterns, and vice-versa. We still have to explain why we require for a constructor pattern 𝒞(p1,,pn) that all FVo(pi) have to be the empty set. The reason for this restriction lies in the following semantic equivalence, which is motivated by its importance for rewriting patterns into a normal form (see Section 5.1) and which we will prove below.

¬𝒞(p1,,pn)¬𝒞(_,,_)𝒞(¬p1,,_)𝒞(_,,¬pn)

The pattern on the right consists of several patterns that are joined by or-patterns. The rules for or-patterns require that each disjunct contains the same sets FVe(). But since the first pattern ¬𝒞(_,,_) doesn’t contain any variables, the other disjuncts must not contain any variables under an even number of negations. And since the subpatterns pi occur under a negation, we can deduce that the patterns pi must not contain any variables under an odd number of negations.

Using linearity we can now prove the missing equivalences which were not included in Theorems 8 and 9.

Theorem 12 (Algebraic equivalences of Patterns, II, [Uncaptioned image]).

For all patterns p,q and r, the following equivalences hold if the patterns on both sides are linear (i.e. 𝐥𝐢𝐧±).

p&(qr)(p&q)(p&r)p(q&r)(pq)&(pr)Distributivityp&pppppIdempotencep&##p__Zeros

Theorem 8 and Theorem 12 taken together show that patterns form a boolean algebra, with the caveat that the conjunction or disjunction of two linear patterns does not necessarily result in a linear pattern. We can also prove some additional equivalences involving constructor patterns.

Theorem 13 (Equivalences of Constructor Patterns, II, [Uncaptioned image]).

For all patterns p1 to pn and p1 to pn and constructors 𝒞𝒞, the following equivalences hold if the patterns on both sides are linear (i.e. 𝐥𝐢𝐧±).

𝒞(p1,,#,,pn) #
𝒞(p1,,pn)&𝒞(p1,,pm) #
𝒞(p1,,pn)&¬𝒞(p1,,pm) 𝒞(p1,,pn)
¬𝒞(p1,,pn)¬𝒞(_,,_)𝒞(¬p1,,pn)𝒞(p1,,¬pn)

The equivalences of Theorem 13 are essential for proving the correctness of the normalization algorithm specified in Section 5.

3.3 Deterministic Patterns

We also have to introduce a notion of deterministic patterns. To see why this is necessary, consider the pathological pattern 𝙿𝚊𝚒𝚛(x,_)𝙿𝚊𝚒𝚛(_,x). This pattern is linear, but when we match against the value 𝙿𝚊𝚒𝚛(2,4) we can either bind the value 2 or the value 4 to the variable x. Deterministic patterns guarantee that when there are multiple derivations which show that a pattern matches against a value, then these alternatives must yield equivalent substitutions333Most languages which implement or-patterns do not check whether patterns are deterministic and instead evaluate or-patterns left-to-right, but this invalidates algebraic laws such as the commutativity of or-patterns. If we need such left-biased or-patterns pq then we can easily define them as p(¬p&q). .

To define deterministic patterns we first have to introduce overlapping and disjoint patterns. In the extended version [2] we give a procedure for deciding whether two patterns in a certain normalized form – which we will introduce in Section 5 – overlap.

Definition 14 (Overlapping and Disjoint Patterns).

Two patterns p and q are overlapping, written pq, if there exists a value v and substitutions σ1 and σ2 such that pvσ1 and qvσ2. Two patterns p and q are disjoint, written pq if such a value and such substitutions do not exist.

We write p𝐝𝐞𝐭 for patterns which satisfy the determinism restriction specified in Figure 2. Note that there are two rules for or-patterns: The rule D-Or1 says that an or-pattern is deterministic if the subpatterns are disjoint, whereas the rule D-Or2 allows arbitrary deterministic subpatterns as long as they do not contain free variables. As for the linearity restriction, these two rules for or-patterns give rise to corresponding rules for and-patterns via De Morgan duality: Given that the pattern ¬(p1p2) is deterministic, we want the semantically equivalent pattern ¬p1&¬p2 to be deterministic as well.

Theorem 15 (Matching for wellformed patterns is deterministic, [Uncaptioned image]).

For any pattern p, value v and substitutions σ1,σ2:

  1. 1.

    If p𝐥𝐢𝐧+, p𝐝𝐞𝐭 and pvσ1 and pvσ2, then σ1σ2.

  2. 2.

    If p𝐥𝐢𝐧, p𝐝𝐞𝐭 and pvσ1, and pvσ2, then σ1σ2.

This property is also necessary to show that the evaluation of terms is deterministic, since we use the relation pvσ when we define the evaluation of pattern matches in the next subsection.

4 Terms and Types

In this section we present the second part of our solution: default clauses. We present a small language, specify its operational semantics and introduce the typing rules for patterns and expressions. The language contains only data types and pattern matching and omits higher-order functions, but these could be added in the standard way.

4.1 The Syntax of Expressions

Expressions e consist of variables x, the application of a constructor to arguments 𝒞(e1,,en) and case expressions. Every case expression consists of a scrutinee, a list of normal clauses c which all have the form pe, and a default clause 𝐝𝐞𝐟𝐚𝐮𝐥𝐭e.

ex𝒞(e1,,en)𝐜𝐚𝐬𝐞e𝐨𝐟{c1,,cn;𝐝𝐞𝐟𝐚𝐮𝐥𝐭e}ExpressioncpeClause

Every case expression contains exactly one default clause, but a realistic language would also allow to omit the default clause if all other clauses are exhaustive. As we already mentioned, 𝐝𝐞𝐟𝐚𝐮𝐥𝐭 does not belong to the syntax of patterns, but to the syntax of case expressions. In particular, the meaning of a default clause is not equal to the meaning of a clause _ed which uses a wildcard pattern, but equal to a clause ¬p1&&¬pned, where the pi are all the patterns from the other clauses of the case expression. For this reason it is not possible to define the meaning of a default clause in isolation, unlike for normal clauses, and a default clause also does not immediately give rise to a valid equation for term rewriting.

4.2 Operational Semantics

We formalize a call-by-value semantics in which the only values v are constructors applied to other values. We use evaluation contexts E and a congruence rule E-Cong to specify evaluation within a subterm, following standard practice [9].

E𝒞(v1,,vn,E,e1,,em)𝐜𝐚𝐬𝐞E𝐨𝐟{c1,,cn;𝐝𝐞𝐟𝐚𝐮𝐥𝐭e}

We write ere for the single-step reduction of the expression e to e which does not use the congruence rule, and ee for the single step reduction within an evaluation context. The evaluation of case expressions is governed by the two rules E-Match and E-Default. The rule E-Match allows to reduce a case expression using any of the normal clauses that match the scrutinee; in particular, it is not necessary to try the clauses in the order in which they occur. On the right-hand side of that rule we write eσ for the result of applying the substitution σ to the expression e. The rule E-Default allows to reduce a case expression to the expression of the default clause if none of the other clauses match against the scrutinee.

Single-step evaluation: ee

Nothing in these rules guarantees that evaluation is deterministic. For example, we can write a case expression with overlapping patterns. If we want evaluation to be deterministic, then we have to check that expressions are wellformed, written as e. For this we have to check that all patterns which occur in case expressions are deterministic, positively linear, and that no two clauses in a case expression have overlapping patterns.

Wellformed expressions: e

The evaluation of wellformed expressions is deterministic. Furthermore, to check whether an expression is wellformed does not depend on any type information.

Theorem 16 (Evaluation of wellformed expressions is deterministic).

For all expressions e which are wellformed (that is e holds), evaluation is deterministic: If ee1 and ee2, then e1=e2.

Multi-step evaluation is defined as the reflexive-transitive closure of single-step evaluation and allows us to define when two closed expressions are semantically equivalent:

Definition 17 (Semantic Equivalence of Expressions).

Two closed expressions e and e are semantically equivalent, written ee, if either both of them evaluate to the same value v, i.e. ev and ev, or if both expressions diverge.

Theorem 18 (Permutation of clauses does not change semantics).

The meaning of a wellformed pattern match does not change when we permute clauses. If c¯ is a permutation of the list of clauses c¯, then 𝐜𝐚𝐬𝐞v𝐨𝐟{c¯;𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed}𝐜𝐚𝐬𝐞v𝐨𝐟{c¯;𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed}.

This theorem makes formal what we call pure no-overlap semantics for pattern matching. The next theorem guarantees that we can use algebraic principles when reasoning about patterns in clauses.

Theorem 19 (Substitution of equivalent patterns preserves semantics).

If p1p2, then

𝐜𝐚𝐬𝐞v𝐨𝐟{p1e,c¯;𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed}𝐜𝐚𝐬𝐞v𝐨𝐟{p2e,c¯;𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed}.

4.3 Typing Rules

In this section, we are going to introduce types, typing contexts and (pattern) typing rules. In order to illustrate the typing rules we include three simple data types: the boolean type 𝔹 with constructors 𝚃𝚛𝚞𝚎 and 𝙵𝚊𝚕𝚜𝚎, the type of pairs ττ with the constructor 𝙿𝚊𝚒𝚛, and the type of binary sums ττ with constructors ι1 and ι2.

τ𝔹ττττTypesΓ,ΔΓ,x:τTyping contexts

We use the judgment Γ;Δp:τ to express that the pattern p matches against a value of type τ and binds variables whose types are recorded in the context Γ on a successful match, and in context Δ in the case of a non-successful match. In other words, the typing context Γ is used for typing the pattern variables that occur under an even number of negations, and the typing context Δ is used for typing the variables under an odd number of negations. We write Γ1++Γ2 for the naive concatenation of two typing contexts.

Pattern typing: Γ;Δp:τ

These pattern typing rules do not make sense on their own, but only in conjunction with the rules for linearity. For example, the typing rules allow to derive the typing judgment x:τ1,x:τ2;𝙿𝚊𝚒𝚛(x,x):τ1τ2, but the linearity rules disallow this pattern444 An implementation might choose to check linearity and typing of patterns simultaneously, but we prefer to present them separately to illustrate that we can also use algebraic patterns in the untyped setting. . Lastly, we present the rules for typing expressions:

Expression typing: Γe:τ


In the rule T-Match we use the pattern typing rule for the pattern in each clause, and add the resulting typing context Γi which contains the types of all pattern variables under an even number of negations to the outer typing context Γ in the respective clause.

5 Normalizing Algebraic Patterns

So far, we have considered algebraic patterns in their unrestricted form which is written by the programmer and checked by the compiler. In this section, we consider normal forms for patterns which simplify the compilation algorithm in Section 6. We end up with a normal form that is a variant of disjunctive normal forms, but which takes the use of constructors and variables into account. We describe a normalization procedure which proceeds in three steps: The first step, described in Section 5.1, pushes negation-patterns inward so that they only occur applied to constructors without subpatterns, or to variables. The second step, described in Section 5.2, pushes or-patterns outwards, so that we end up with a disjunctive normal form, that is a disjunction of elementary conjuncts. In the third step, we further simplify these elementary conjuncts; this is described in Section 5.3. The default clause is not affected by the normalization procedure described in this section; the compilation of case expressions to decision trees described in the next section can handle default clauses natively.

5.1 Computing the Negation Normal Form

Consider the pattern ¬𝙿𝚊𝚒𝚛(𝚃𝚛𝚞𝚎,𝙵𝚊𝚕𝚜𝚎) which matches anything except a tuple consisting of the boolean values 𝚃𝚛𝚞𝚎 and 𝙵𝚊𝚕𝚜𝚎. Another way to express the behavior of this pattern is to say that it matches anything which isn’t a tuple, or it matches a tuple if either the left element isn’t the value 𝚃𝚛𝚞𝚎 or the right element isn’t the value 𝙵𝚊𝚕𝚜𝚎. We can therefore also write the pattern as ¬𝙿𝚊𝚒𝚛(_,_)𝙿𝚊𝚒𝚛(¬𝚃𝚛𝚞𝚎,_)𝙿𝚊𝚒𝚛(_,¬𝙵𝚊𝚕𝚜𝚎). We can generalize this observation and use it to push all negation-patterns inside, by repeatedly rewriting the pattern using the last equation of Theorem 13. After we have normalized patterns in this way, negation-patterns only occur applied to variables or to constructors where all subpatterns of the constructor are wildcard patterns. We abbreviate these negated constructor patterns ¬𝒞n(_,,_) as ¬𝒞n.

Definition 20 (Negation Normal Forms).

The syntax of negation normal forms is:

Nx¬x¬𝒞n𝒞n(N1,,Nn)N&NNN_#
𝚗𝚗𝚏(p1&p2)𝚗𝚗𝚏(p1)𝚗𝚗𝚏(p2)𝚗𝚗𝚏+(p1&p2)𝚗𝚗𝚏+(p1)&𝚗𝚗𝚏+(p2)𝚗𝚗𝚏(p1p2)𝚗𝚗𝚏(p1)&𝚗𝚗𝚏(p2)𝚗𝚗𝚏+(p1p2)𝚗𝚗𝚏+(p1)𝚗𝚗𝚏+(p2)𝚗𝚗𝚏(¬p)𝚗𝚗𝚏+(p)𝚗𝚗𝚏+(¬p)𝚗𝚗𝚏(p)𝚗𝚗𝚏(#)_𝚗𝚗𝚏+(#)#𝚗𝚗𝚏(_)#𝚗𝚗𝚏+(_)_𝚗𝚗𝚏(x)¬x𝚗𝚗𝚏+(x)x
𝚗𝚗𝚏(𝒞n(p1,,pn))¬𝒞n𝒞n(𝚗𝚗𝚏(p1),,_)𝒞n(_,,𝚗𝚗𝚏(pn))𝚗𝚗𝚏+(𝒞n(p1,,pn))𝒞n(𝚗𝚗𝚏+(p1),,𝚗𝚗𝚏+(pn))

The normalization procedure 𝚗𝚗𝚏() is split into the two functions 𝚗𝚗𝚏() and 𝚗𝚗𝚏+() which track if we are currently normalizing a negated or a non-negated pattern. At the top-level, the function 𝚗𝚗𝚏() therefore just invokes 𝚗𝚗𝚏+().

Example 21.

We compute the negation normal form of our running example.

𝚗𝚗𝚏+(x&(𝚂𝚊𝚂𝚞))=x&(𝚂𝚊𝚂𝚞)𝚗𝚗𝚏+(x&¬(𝚂𝚊𝚂𝚞))=x&(¬𝚂𝚊)&(¬𝚂𝚞)

We treat the negation normal forms N as a subset of patterns p, and hence all functions and properties of patterns can also be applied to normal forms. If we translate a pattern into negation normal form then the pattern is still wellformed, can be typed using the same context and type, and has the same semantics as before. This is witnessed by the following lemma.

Lemma 22 (Negation Normalization Preserves Linearity, Typing and Semantics).

For all patterns p, contexts Γ,Δ and types τ, we have:

  1. 1.

    Linearity is preserved: If p𝐥𝐢𝐧+, then 𝚗𝚗𝚏(p)𝐥𝐢𝐧+.

  2. 2.

    Typing is preserved: If Γ;Δp:τ, then Γ;Δ𝚗𝚗𝚏(p):τ.

  3. 3.

    The patterns are semantically equivalent: If p𝐥𝐢𝐧±, then p𝚗𝚗𝚏(p).

Proof.

Available in the appendix of the extended version of this paper [2].

5.2 Computing the Disjunctive Normal Form

The next step is to bring the negation normal form into a disjunctive normal form. Patterns in disjunctive normal form consist of an outer disjunction of so-called “elementary conjuncts” which do not contain or-patterns. The syntax of the disjunctive normal forms D and elementary conjuncts K is given below. We can normalize a pattern in negation normal form to the disjunctive normal form by the function 𝚍𝚗𝚏() which takes a pattern in negation normal form and returns a set of elementary conjuncts. At the top-level we have to wrap the result in a {} node.

Definition 23 (Disjunctive Normal Form).
D{K1,,Kn}Disjunctive Normal FormKx¬x¬𝒞n𝒞n(K1,,Kn)K&K_#Elementary Conjunct
𝚍𝚗𝚏(x){x}𝚍𝚗𝚏(¬x){¬x}𝚍𝚗𝚏(¬𝒞n){¬𝒞n}𝚍𝚗𝚏(_){_}𝚍𝚗𝚏(#){#}
𝚍𝚗𝚏(𝒞n(N1,,Nn)){𝒞(k1,,kn)k1𝚍𝚗𝚏(N1),,kn𝚍𝚗𝚏(Nn)}𝚍𝚗𝚏(N1&N2){k1&k2k1𝚍𝚗𝚏(N1),k2𝚍𝚗𝚏(N2)}𝚍𝚗𝚏(N1N2)𝚍𝚗𝚏(N1)𝚍𝚗𝚏(N2)
Example 24.

We compute the disjunctive normal form for our running example.

𝚍𝚗𝚏(x&(𝚂𝚊𝚂𝚞))={x&𝚂𝚊,x&𝚂𝚞}𝚍𝚗𝚏(x&(¬𝚂𝚊)&(¬𝚂𝚞))={x&(¬𝚂𝚊)&(¬𝚂𝚞)}

We can embed the disjunctive normal form {K1,,Kn} into patterns as K1Kn. This set of conjuncts is by design never empty (n>0), however, if we do admit this case (n=0) it translates to the absurd-pattern # and is treated as such in any use case (see Section 6).

5.3 Normalizing Elementary Conjuncts

As a last step, we further simplify the elementary conjuncts of Section 5.2. We write D¯ and K¯ for these normalized disjunctive normal forms and normalized elementary conjuncts:

Definition 25 (Normalized Disjunctive Normal Form).
D¯{K¯1,,K¯n}K¯{x1,,xn}&𝒞(K¯1,,K¯m){x1,,xn}&¬{𝒞1,,𝒞m}{x1,,xn}&#

These normalized conjuncts follow from the observation that there are essentially only three different kinds of conjuncts: positive, negative and unsatisfiable. Each of these three conjuncts can bind a set of variables {x1,,xn}. A positive conjunct {x1,,xn}&𝒞(K¯1,,K¯m) matches against any value headed by the constructor 𝒞, and binds that value against all the variables in the set. A negative conjunct {x1,,xn}&¬{𝒞1,,𝒞m} matches against any value which is headed by a constructor which is not in the list {𝒞1,,𝒞m}. The negative conjunct {x}&¬{} can be used to represent a single pattern variable x, and similarly {}&¬{} can be used to represent a wildcard pattern. An unsatisfiable conjunct {x1,,xn}&# never matches against a value.

Computing these normalized conjuncts consists in collecting all the variables, as well as all negated and non-negated head constructors of an elementary conjunct. Intuitively, the head constructors are constructors that occur in a pattern without being nested in a subpattern of another constructor. An elementary conjunct is unsatisfiable if two different non-negated constructors appear in the conjunct, or if a head constructor appears both negated and non-negated. An elementary conjunct can be simplified to a negative conjunct if only negated head constructors appear in the original conjunct. Lastly, an elementary conjunct can be simplified to a positive conjunct if neither of the two cases above apply. We write 𝚗𝚘𝚛𝚖() for the function which computes these normalized conjuncts; the formal definition of this function is given in the appendix of the extended version of this paper [2]. We also remove negated variables in this step and treat them like absurd patterns; this is, strictly speaking, not semantics preserving, but negated variables cannot be used in the right-hand side of a clause anyway, so this has no effect on the compilation of programs.

Example 26.

We compute the normalized disjunctive normal form for our running example.

𝚗𝚘𝚛𝚖({x&𝚂𝚊,x&𝚂𝚞}) ={{x}&𝚂𝚊,{x}&𝚂𝚞}
𝚗𝚘𝚛𝚖({x&(¬𝚂𝚊)&(¬𝚂𝚞)}) ={{x}&¬{𝚂𝚊,𝚂𝚞}}

Like in Section 5.2, we treat the normal forms D¯ and K¯ as subsets of patterns p. We use patterns in normalized disjunctive normal form in the next section where we define a pattern matching compilation algorithm for nested patterns.

6 Compiling Algebraic Patterns

We have shown that algebraic patterns are more expressive because they explicitly allow us to speak about the complement of a pattern. But programmers won’t use them if they have to pay for the gain in expressivity with slower programs. In order to compile programs using these patterns to efficient code we therefore have to use and modify one of the available pattern matching compilation algorithms.

These algorithms can be divided into two general classes: Algorithms which use backtracking [1] and algorithms which compile to decision trees [23]. The algorithms which use backtracking guarantee that the size of the generated code is linear in the size of the input, but the generated code might perform redundant tests. Algorithms which compile to decision trees never examine scrutinees more than once but the generated code might be exponential in the size of the input. Both classes of algorithms are competitive when they are optimized [20, 23], so it is not possible to say which alternative is preferable in general.

In this article we modify and extend the compilation scheme laid out by Maranget [23] which is based on decision trees. His algorithm already includes a treatment of or-patterns, and we will extend it to the normalized patterns that we introduced in Section 5. We introduce the general idea of the compilation to decision trees in Section 6.1, present the central branching step in Section 6.2 in detail and illustrate the algorithm with an example in Section 6.3. We conclude with a statement on the correctness of our extended compilation scheme in Section 6.4.

6.1 General Idea of the Algorithm

The algorithm operates on a generalization of the case expression 𝐜𝐚𝐬𝐞v𝐨𝐟{p1e1,,pnen;𝐝𝐞𝐟𝐚𝐮𝐥𝐭e} that we introduced in Section 4.1. Instead of only one scrutinee v and one pattern p in each clause we now allow a vector v1,,vn of scrutinees and a matrix of clauses with patterns in nDNF (see Section 5.3).

𝐜𝐚𝐬𝐞v1,,vn𝐨𝐟[D¯11D¯n1e1D¯1mD¯nmem𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed] (Input)

In such multi-column case expressions, the i-th pattern column tests the i-th scrutinee vi. The main idea of the algorithm is that we take such a multi-column case expression as input and examine all scrutinees – and subscrutinees – one by one, producing a nesting of single-column case expressions with simple patterns as output. These simple patterns consist only of constructors applied to variables. We start the compilation by embedding an ordinary case expression into this generalized form on which we call the function compile. This function can take one of three steps. It either terminates with a Default or Simple step, or it examines one of the scrutinees in a Branch step.

Default

If 𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed is the only clause then we terminate with the expression ed.

Simple

If the first row consists only of variable patterns of the form {x1,xk}&¬{} then compilation terminates with the right-hand side of that clause together with appropriate substitutions. This rule also applies if n=0.

Branch

If neither of the above steps apply we pick a column i (with 1in) and examine its scrutinee vi in a single-column case against simple constructor patterns.

All the complexity of the algorithm lies within the Branch step, so we will discuss its details in the next subsection.

6.2 The Branching Step

The first step is to choose on which index i we want to perform the case split. This choice does not affect the correctness of the algorithm; selecting a column is a therefore a matter of optimization which other authors [26, 23] have discussed in detail. We only enforce that the selected pattern column must contain outermost constructors. Such a column must necessarily exist if neither the Default nor the Simple step apply.

Next, we have to gather the constructors against which we can test the scrutinee vi. We collect these so-called head constructors into a set (D¯i) defined below.

(D¯i):=j=1,,m𝚑𝚎𝚊𝚍(D¯ij)with𝚑𝚎𝚊𝚍({K¯1,,K¯t})l=1,,t𝚑𝚎𝚊𝚍(K¯l)𝚑𝚎𝚊𝚍({x1,,xs}&𝒞(K¯1,,K¯t)){𝒞}𝚑𝚎𝚊𝚍({x1,,xs}&¬{𝒞1,,𝒞t}){𝒞1,,𝒞t}𝚑𝚎𝚊𝚍({x1,,xs}&#)

Once we have found these head constructors {𝒞n1,,𝒞nz} we generate fresh pattern variables x1k to xnkk for each constructor 𝒞nk and a simple pattern match. For each clause we have gained information about the scrutinee vi: Within the right-hand side of a constructor clause, we know that our scrutinee must have conformed to the shape of our constructor. Likewise, if we arrive at the right-hand side of the default clause, we know that our scrutinee must have been different to all the constructors {𝒞n1,,𝒞nz} before. We use this knowledge by computing clause-specific subproblems of our initial Input and we do so via the function 𝒮 for constructor clauses and function 𝒟 for the default clause.

𝐜𝐚𝐬𝐞vi𝐨𝐟[𝒞n1(x11,,xn11)𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝒮(i,𝒞n1(x11,,xn11),Input))𝒞nz(x1z,,xnzz)𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝒮(i,𝒞nz(x1z,,xnzz),Input))𝐝𝐞𝐟𝐚𝐮𝐥𝐭𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝒟(i,{𝒞n1,,𝒞nz},Input))] (Output)

All that is left is to compute these subproblems of specialization and default which are the arguments of recursive invocations of the compile function.

6.2.1 Computing Specialization

We now show how to compute 𝒮(i,𝒞k(x1,,xk),Input) which specializes Input into a constructor-specific subproblem that utilizes the assumption that the scrutinee vi has already matched the constructor pattern 𝒞k(x1,,xk). We therefore remove the old scrutinee vi and replace it with the bound variables x1,,xk as new scrutinees.

𝒮(i,𝒞k(x1,,xk),Input)=𝐜𝐚𝐬𝐞x1,,xk,v1,,vi1,vi+1,,vn𝐨𝐟[S]

We now have to build a new clause matrix [S] that matches against the new list of scrutinees and do so by iterating over the previous clauses. Each clause of the old Input contributes to [S] only if its i-th pattern could have matched the old scrutinee vi of shape 𝒞k by assumption. This is, of course, the case for the default clause which we keep unchanged. Otherwise, these i-th patterns are in normalized DNF, and we have to check whether one of its elementary conjuncts could have matched. The following list gives all inclusion rules.

  1. 1.

    Row [𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed] is added to [S] unchanged.

  2. 2.

    Row [D¯1jD¯i1j𝐃¯𝐢𝐣D¯i+1jD¯njej] only contributes according to 𝐃¯𝐢𝐣:

    1. (a)

      If 𝐃¯𝐢𝐣={y1,ys}&𝒞k(K¯1,,K¯k),
      then add the row [K¯1K¯kD¯1jD¯i1jD¯i+1jD¯njej[{y1,ys}vi]].

    2. (b)

      If 𝐃¯𝐢𝐣={y1,ys}&¬{𝒞1,,𝒞t} and 𝒞k{𝒞1,,𝒞t},
      then add the row [__D¯1jD¯i1jD¯i+1jD¯njej[{y1,ys}vi]].
      (Note that we use _ simply as a shorthand for the equivalent nDNF {}&¬{})

    3. (c)

      If 𝐃¯𝐢𝐣={K¯1,,K¯t}, then
      recursively iterate the rows of [D¯1jD¯i1j𝐊¯1D¯i+1jD¯njejD¯1jD¯i1j𝐊¯tD¯i+1jD¯njej].

We give an example for case 2b which handles negation. Suppose that want to specialize the following case expression into a case expression subproblem which assumes that v1 was matched against 𝙲𝚘𝚗𝚜(x,y).

compile(casev1,v2of[{}&¬{𝙲𝚘𝚗𝚜}D¯21e1{}&¬{𝙽𝚒𝚕}D¯22e2defaulted])=casev1of[𝙲𝚘𝚗𝚜(x,y)compile(subproblem)]

The first clause is discarded, because the pattern {}&¬{𝙲𝚘𝚗𝚜} is incompatible with the assumption that vi matches 𝙲𝚘𝚗𝚜. The second clause using {}&¬{𝙽𝚒𝚕} is compatible with the assumption. So, how can we transform this clause into a new one testing x,y and v2? We see that pattern {}&¬{𝙽𝚒𝚕} does not match on the subvalues x and y of v1, but for v2 we still have to match against the pattern D¯22. We keep the default clause by rule 1 unchanged.

subproblem=casex,y,v2of[__D¯22e2defaulted]

6.2.2 Computing Default

Next we show how to compute 𝒟(i,{𝒞n1,,𝒞nz},Input) whose output reflects the assumption that the scrutinee vi did not match any of the head constructors 𝒞n1 to 𝒞nz. This time we have no new scrutinees and only match the leftover list against a new pattern matrix [D]:

𝒟(i,{𝒞n1,,𝒞nz},Input)=𝐜𝐚𝐬𝐞v1,,vi1,vi+1,,vn𝐨𝐟[D]

As before, we build a new matrix [D] by row-wise iteration over the clauses of the Input. A clause only contributes if its i-th pattern entry allows for a match with scrutinee vi, knowing that it differs from all the head constructors Cn1,,Cnz.

  1. 1.

    Row [𝐝𝐞𝐟𝐚𝐮𝐥𝐭ed] is added to [D] unchanged.

  2. 2.

    Row [D¯1jD¯i1j𝐃¯𝐢𝐣D¯i+1jD¯njej], only contributes according to 𝐃¯𝐢𝐣:

    1. (a)

      If 𝐃¯𝐢𝐣={y1,,ys}&¬{𝒞1,,𝒞t},
      then add the row [D¯1jD¯i1jD¯i+1jD¯njej[{y1,ys}vi]].
      Note that vi’s constructor differs from all head constructors which include {𝒞1,,𝒞t}

    2. (b)

      If 𝐃¯𝐢𝐣={K¯1,,K¯t}, then
      recursively iterate the rows of [D¯1jD¯i1j𝐊¯1D¯i+1jD¯njejD¯1jD¯i1j𝐊¯tD¯i+1jD¯njej].

Let us illustrate rule 2a with the case expression below, where we perform a case split on v1. We want to construct the default subproblem which assumes that v1 did not match the constructors 𝚁𝚎𝚍 or 𝙶𝚛𝚎𝚎𝚗.

compile(casev1,v2of[{}&𝚁𝚎𝚍D¯21e1{}&¬{𝙶𝚛𝚎𝚎𝚗}D¯22e2defaulted])=casev1of[𝚁𝚎𝚍𝙶𝚛𝚎𝚎𝚗defaultcompile(subproblem)]

In subproblem, we have no new scrutinees and only need to investigate the remaining scrutinee v2 subproblem=casev2of[D]. We ask for each clause if it is consistent with v1 not looking like either 𝚁𝚎𝚍 or 𝙶𝚛𝚎𝚎𝚗. The first clause could not match {}&𝚁𝚎𝚍, so we discard it. For the second row, however, the test {}&¬{𝙶𝚛𝚎𝚎𝚗} does match any v1 that differs from both 𝚁𝚎𝚍 and 𝙶𝚛𝚎𝚎𝚗, so we keep it. The default clause is also kept unchanged.

subproblem=casev2of[D¯22e2defaulted]

6.3 Compiling an Example

Let us illustrate the compilation process with a variant of the example introduced in Section 3.

𝐜𝐚𝐬𝐞x𝐨𝐟{y&(𝚂𝚊𝚂𝚞)“Today is weekend!”,y&¬(𝙵𝚛𝚂𝚊𝚂𝚞)“Today is ”++show(y),𝐝𝐞𝐟𝐚𝐮𝐥𝐭“Tomorrow is weekend…”}

First, we rewrite this case expression into the multi-column notation, normalize all patterns (see Section 5), and abbreviate the right-hand sides by e1(y), e2(y), and d.

𝐼𝑛𝑝𝑢𝑡:=𝐜𝐚𝐬𝐞x𝐨𝐟[{{y}&𝚂𝚊,{y}&𝚂𝚞}e1(y){{y}&¬{𝙵𝚛,𝚂𝚊,𝚂𝚞}}e2(y)𝐝𝐞𝐟𝐚𝐮𝐥𝐭d]

We branch on the only available scrutinee x by matching it in a single-column case expression against the head constructors {𝙵𝚛,𝚂𝚊,𝚂𝚞}. We then generate a new case expression with four clauses, one for each constructor. All that is left to do is to compute and compile the clause-specific subproblems of specialization and default.

𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝐼𝑛𝑝𝑢𝑡)=𝐜𝐚𝐬𝐞x𝐨𝐟[𝙵𝚛𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝒮(1,𝙵𝚛,𝐼𝑛𝑝𝑢𝑡))𝚂𝚊𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝒮(1,𝚂𝚊,𝐼𝑛𝑝𝑢𝑡))𝚂𝚞𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝒮(1,𝚂𝚞,𝐼𝑛𝑝𝑢𝑡))𝐝𝐞𝐟𝐚𝐮𝐥𝐭𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝒟(1,{𝙵𝚛,𝚂𝚊,𝚂𝚞},𝐼𝑛𝑝𝑢𝑡))]

Let us consider the subproblem 𝒮(1,𝚂𝚊,𝐼𝑛𝑝𝑢𝑡). We know that scrutinee x has matched 𝚂𝚊, and we have no left-over scrutinees nor new subscrutinees to consider (n=0), giving 𝒮(1,𝚂𝚊,𝐼𝑛𝑝𝑢𝑡)=caseof[S]. We get the new clause matrix [S] by iterating the old one to see how each row contributes.

  • For row [{{y}&𝚂𝚊,{y}&𝚂𝚞}e1(y)], by rule (c), iterate [{y}&𝚂𝚊e1(y){y}&𝚂𝚞e1(y)].

    • For the first one, by (a) we add the row [e1(x)].

    • For the second one, no contribution is made.

  • For row [{{y}&¬{𝙵𝚛,𝚂𝚊,𝚂𝚞}}e2(y)] no contribution is made.

  • For row [𝐝𝐞𝐟𝐚𝐮𝐥𝐭d], by (1), we keep it unchanged.

This gives the specialization matrix [S]=[e1(x)𝐝𝐞𝐟𝐚𝐮𝐥𝐭d].

The other specialization subproblems are computed similarly, and we obtain the following intermediate result.

𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝐼𝑛𝑝𝑢𝑡)=𝐜𝐚𝐬𝐞x𝐨𝐟[𝙵𝚛𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝐜𝐚𝐬𝐞𝐨𝐟[𝐝𝐞𝐟𝐚𝐮𝐥𝐭d]),𝚂𝚊𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝐜𝐚𝐬𝐞𝐨𝐟[e1(x)𝐝𝐞𝐟𝐚𝐮𝐥𝐭d])𝚂𝚞𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝐜𝐚𝐬𝐞𝐨𝐟[e1(x)𝐝𝐞𝐟𝐚𝐮𝐥𝐭d])𝐝𝐞𝐟𝐚𝐮𝐥𝐭𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝐜𝐚𝐬𝐞𝐨𝐟[e2(x)𝐝𝐞𝐟𝐚𝐮𝐥𝐭d])]

For each of the subproblems we can apply either Default or Simple and conclude the computation.

𝑐𝑜𝑚𝑝𝑖𝑙𝑒(𝐼𝑛𝑝𝑢𝑡)=𝐜𝐚𝐬𝐞x𝐨𝐟[𝙵𝚛d𝚂𝚊e1(x)𝚂𝚞e1(x)𝐝𝐞𝐟𝐚𝐮𝐥𝐭e2(x)]

Note how the right-hand expression of the previous default clause d is utilized in a constructor-clause, whereas the right-hand expression of the new default-clause originates from a previously non-default clause.

6.4 Correctness of Compilation

Correctness of compilation states that the compile function introduced in Section 6 preserves semantics. Because the compilation algorithm operates on multi-column pattern matches, we extend wellformedness of expressions, the operational semantics and denotations to multi-column pattern matches. We can then state the correctness for the compilation algorithm:

Theorem 27.

On a wellformed multi-column pattern match of the normalized shape Input, the compilation algorithm preserves semantics: Inputcompile(Input).

We provide the proof and the extended definitions in the appendix of the extended version of this paper [2].

7 Future Work

There are two main directions that we plan to investigate in future work.

Allowing Overlapping Patterns

In this article we enforce that no two patterns in a pattern match can overlap in order to guarantee confluence of the system, but [7, 8] showed that this restriction can be relaxed. They allow overlapping patterns under the condition that the right-hand sides of the clauses must become judgmentally equal under a substitution computed from the overlapping patterns. Let us look at an example that is allowed in their system, but not ours:

or(x)𝐜𝐚𝐬𝐞x𝐨𝐟{ 𝙿𝚊𝚒𝚛(𝚃𝚛𝚞𝚎,_)𝚃𝚛𝚞𝚎, 𝙿𝚊𝚒𝚛(_,𝚃𝚛𝚞𝚎)𝚃𝚛𝚞𝚎,
𝙿𝚊𝚒𝚛(𝙵𝚊𝚕𝚜𝚎,x)x, 𝙿𝚊𝚒𝚛(x,𝙵𝚊𝚕𝚜𝚎)x}

In this example, the first and the last (as well as the second and first, second and third, and the last two) patterns are overlapping. But the definition of this function is still confluent, since if we unify the overlapping patterns 𝙿𝚊𝚒𝚛(True,_) and 𝙿𝚊𝚒𝚛(x,False), and apply the substitution to the right-hand sides True and x, the terms become judgmentally equal. Such overlapping patterns can be useful in a proof assistant, since they yield additional judgmental equalities that can be used to simplify a term during normalization. We think that their approach to allow overlapping patterns and our algebraic patterns can complement each other, and we therefore plan to study their interaction.

Exhaustiveness Checking

In this article, we check whether a set of patterns is overlapping, but we do not check whether a set of patterns is exhaustive. This is in spite of the fact that we are compiling these patterns using decision trees, which is a method that can easily accommodate checking the exhaustiveness of sets of patterns [22]. Checking that a given set of patterns is exhaustive is essential for real programming languages, so we plan to develop and verify an algorithm for this purpose, building on the work of [22, 15, 12]. We sketch a basic outline for such an algorithm in the appendix of the extended version of this paper [2].

8 Related Work

Order-Independent Pattern Matching

As discussed in Section 7, [7] and [8] study the issue of order-independent pattern matching in proof assistants. They want to ensure that every clause in a pattern-matching expression can be used as a judgmental equality for normalizing terms during type checking. Their system lacks advanced patterns such as or-patterns and negation patterns, but they instead relax the non-overlap requirement.

Substructural logics

And-patterns and wildcard patterns are strongly related to the rules of contraction and weakening in the sequent calculus. This relationship between structural rules and certain forms of patterns has been observed by [4], who present a pattern matching calculus for the sequent calculus which includes both and-patterns and wildcard-patterns. This relationship can also be observed in programming languages with substructural type systems, such as Rust, where the expression 𝚖𝚊𝚝𝚌𝚑𝚟{𝚡@𝚢=>(𝚡,𝚢)} does not type check if the type of v does not support contraction, i.e. does not implement the Copy trait.

Logic Programming and Datalog

The problems with the binding structure of patterns involving negations that we discussed in this paper appears in a similar form for Datalog programs. In this article we are only interested in the binding occurrences of variables in a pattern, i.e. those variables which occur under an even number of negations, and which can be used in the right-hand side of a clause. Occurrences of logic variables in a Datalog program can be distinguished into binding occurrences and bound occurrences, where binding occurrences assign a value to a logic variable and bound occurrences can access the value the variable is bound to. The authors of [18] describe a type system which tracks binding and bound occurences of variables and describe the rules for disjunctions and negations in their Section 4.3. Their rule for negations is:

Here # stands for a polarity, either + or , which controls which variables in the input context Γ1 are already bound, and the multiplication with correspondingly switches this interpretation. This rule therefore operates very similarly to our pattern typing rule P-Neg in Section 4.3 and guarantees that negation is involutive with respect to the typing relation.

Dynamic First Class Patterns

We use “dynamic first class patterns” for systems in which patterns can be computed and passed at runtime. Such systems, whose theory was studied by [14] and [13], can also express our algebraic patterns. The technical report [11] describes an extension of the object-oriented language Newspeak (cf. [3]) by such a system of dynamic pattern matching. They explicitly discuss various pattern combinators, i.e. combinators which combine patterns to yield new ones. They discuss disjunction, conjunction and negation pattern combinators which correspond to our or-patterns, and-patterns and negation patterns. In this article, we focus exclusively on statically-known patterns, since these can be analyzed and compiled more easily.

Negation Patterns

Negation patterns also appear in [19], where the author mentions that “Having false- and or-patterns allows us to define the complement of a pattern – for any pattern at a type A, we can define another pattern that matches exactly the values of type A the original does not”. [19] presents a translation that eliminates negation patterns, but this translation makes a closed-world assumption about all the available constructors for each data type. For example, he translates the negated pair pattern ¬𝙿𝚊𝚒𝚛(p1,p2) by the pattern 𝙿𝚊𝚒𝚛(¬p1,_)𝙿𝚊𝚒𝚛(_,¬p2), whereas we (in Section 5.1) translate it by the pattern ¬𝙿𝚊𝚒𝚛(_,_)𝙿𝚊𝚒𝚛(¬p1,_)𝙿𝚊𝚒𝚛(_,¬p2). He can omit the first disjunct of the or-pattern because he knows that the pair type only has one constructor. If we extend his translation scheme to the list type, then we would translate the negation of the constructor-pattern Nil to the pattern Cons(_,_), since we know that the list type only has two constructors. Because we encode negative information directly in patterns, we never have to rely on type information during our compilation process. Negation patterns have also been introduced under the name of “anti-patterns” and “anti-pattern matching” by [16, 17, 6]. The formalism they present differs from ours in the following points: They only allow constructor-patterns, variable patterns and negation patterns, which means that they cannot give a recursive translation for patterns involving negations, since that also requires or-patterns. Their formalism is also more rooted in the theory of term rewriting systems instead of programming languages, and they do not present a compilation algorithm to simple patterns as we do.

Pattern matching compilation algorithms

A simple way to execute pattern matching expressions with nested patterns is to try one clause after another, until the first one matches. This, however, is extremely inefficient if we have multiple clauses which are headed by the same outer constructor. In that case, we are performing the same tests multiple times. As an introduction to this problem, we can still recommend the book by [24, Chapter 5]. There are many algorithms that compile nested pattern matches to efficient code, but most of them fall into one of two categories. Nested pattern matching can be either compiled using backtracking automata [1, 20] or by compiling to decision trees [23]. The main difference between these two approaches is a classical time vs. space trade-off. Compiling to decision trees guarantees that every test is executed at most once, but can potentially increase the size of the generated code; backtracking automata do not increase the size of the code, but may perform some tests multiple times. In Section 6 we present a version of the algorithm by [23] which compiles to decision trees. Recently, Cheng and Parreaux [5] also proved the correctness of a pattern matching compilation algorithm for their very expressive pattern matching syntax; they, however, do not care about order-independence in the same way that we do.

9 Conclusion

Pattern matching is an extremely popular declarative programming construct. We argued that if we want to make pattern matching even more declarative, then the order of the clauses in a program should not matter. But if we require that two patterns must not overlap, and if we want to avoid overly verbose pattern matching expressions, then we have to make the language of patterns and pattern matching more expressive. We introduced two complementary features, a boolean algebra of patterns and default clauses, which together solve the verbosity problem. We have provided the operational and static semantics of these constructs, and have shown that they can be compiled to efficient code.

Data-Availability Statement

The theorems in Section 3 have been formalized and checked in the proof assistant Rocq. These proofs are available as related material.

References

  • [1] Lennart Augustsson. Compiling pattern matching. In Jean-Pierre Jouannaud, editor, Functional Programming Languages and Computer Architecture, pages 368–381, Berlin, Heidelberg, 1985. Springer. doi:10.1007/3-540-15975-4_48.
  • [2] David Binder and Lean Ermantraut. The algebra of patterns (extended version). arXiv, 2025.
  • [3] Gilad Bracha. Newspeak programming language draft specification version 0.104, 2022. Technical report.
  • [4] Serenella Cerrito and Delia Kesner. Pattern matching as cut elimination. Theoretical Computer Science, 323(1):71–127, 2004. doi:10.1016/j.tcs.2004.03.032.
  • [5] Luyu Cheng and Lionel Parreaux. The ultimate conditional syntax. Proc. ACM Program. Lang., 8(OOPSLA2), October 2024. doi:10.1145/3689746.
  • [6] Horatiu Cirstea, Claude Kirchner, Radu Kopetz, and Pierre-Etienne Moreau. Anti-patterns for rule-based languages. Journal of Symbolic Computation, 45(5):523–550, 2010. Symbolic Computation in Software Science. doi:10.1016/j.jsc.2010.01.007.
  • [7] Jesper Cockx. Overlapping and order-independent patterns in type theory. Master’s thesis, KU Leuven, 2013. URL: https://lirias.kuleuven.be/retrieve/262314/.
  • [8] Jesper Cockx, Frank Piessens, and Dominique Devriese. Overlapping and order-independent patterns. In Zhong Shao, editor, Programming Languages and Systems, pages 87–106, Berlin, Heidelberg, 2014. Springer. doi:10.1007/978-3-642-54833-8_6.
  • [9] Matthias Felleisen and Robert Hieb. The revised report on the syntactic theories of sequential control and state. Theoretical Computer Science, 103(2):235–271, 1992. doi:10.1016/0304-3975(92)90014-7.
  • [10] Jacques Garrigue. Programming with polymorphic variants. In ML workshop. Baltimore, 1998. URL: https://caml.inria.fr/pub/papers/garrigue-polymorphic_variants-ml98.pdf.
  • [11] Felix Geller, Robert Hirschfeld, and Gilad Bracha. Pattern Matching for an Object-Oriented and Dynamically Typed Programming Language. Technische Berichte des Hasso-Plattner-Instituts für Softwaresystemtechnik an der Universität Potsdam. Universitätsverlag Potsdam, 2010.
  • [12] Sebastian Graf, Simon Peyton Jones, and Ryan G. Scott. Lower your guards: A compositional pattern-match coverage checker. Proc. ACM Program. Lang., 4(ICFP), August 2020. doi:10.1145/3408989.
  • [13] Barry Jay. Pattern Calculus: Computing with Functions and Structures. Springer, 2009. doi:10.1007/978-3-540-89185-7.
  • [14] Barry Jay and Delia Kesner. First-class patterns. Journal of Functional Programming, 19(2):191–225, 2009. doi:10.1017/S0956796808007144.
  • [15] Georgios Karachalias, Tom Schrijvers, Dimitrios Vytiniotis, and Simon Peyton Jones. Gadts meet their match: Pattern-matching warnings that account for gadts, guards, and laziness. In Proceedings of the 20th ACM SIGPLAN International Conference on Functional Programming, ICFP 2015, pages 424–436, New York, NY, USA, 2015. Association for Computing Machinery. doi:10.1145/2784731.2784748.
  • [16] Claude Kirchner, Radu Kopetz, and Pierre-Etienne Moreau. Anti-pattern matching. In Rocco De Nicola, editor, Programming Languages and Systems, pages 110–124, Berlin, Heidelberg, 2007. Springer. doi:10.1007/978-3-540-71316-6_9.
  • [17] Claude Kirchner, Radu Kopetz, and Pierre-Etienne Moreau. Anti-pattern matching modulo. In Carlos Martín-Vide, Friedrich Otto, and Henning Fernau, editors, Language and Automata Theory and Applications, pages 275–286, Berlin, Heidelberg, 2008. Springer. doi:10.1007/978-3-540-88282-4_26.
  • [18] David Klopp, Sebastian Erdweg, and André Pacak. A typed multi-level datalog ir and its compiler framework. Proc. ACM Program. Lang., 8(OOPSLA2), October 2024. doi:10.1145/3689767.
  • [19] Neelakantan R. Krishnaswami. Focusing on pattern matching. In Proceedings of the 36th Annual ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’09, pages 366–378, New York, NY, USA, 2009. Association for Computing Machinery. doi:10.1145/1480881.1480927.
  • [20] Fabrice Le Fessant and Luc Maranget. Optimizing pattern matching. In Proceedings of the Sixth ACM SIGPLAN International Conference on Functional Programming, ICFP ’01, pages 26–37, New York, NY, USA, 2001. Association for Computing Machinery. doi:10.1145/507635.507641.
  • [21] Justin Lubin and Sarah E. Chasins. How statically-typed functional programmers write code. Proc. ACM Program. Lang., 5(OOPSLA), October 2021. doi:10.1145/3485532.
  • [22] Luc Maranget. Warnings for pattern matching. Journal of Functional Programming, 17(3):387–421, 2007. doi:10.1017/S0956796807006223.
  • [23] Luc Maranget. Compiling pattern matching to good decision trees. In Proceedings of the 2008 ACM SIGPLAN workshop on ML, pages 35–46, 2008. doi:10.1145/1411304.1411311.
  • [24] Simon Loftus Peyton Jones. The Implementation of Functional Programming Languages. Prentice-Hall International Series in Computer Science. Prentice Hall, 1987.
  • [25] Nick Rioux, Xuejing Huang, Bruno C. d. S. Oliveira, and Steve Zdancewic. A bowtie for a beast: Overloading, eta expansion, and extensible data types in f. Proc. ACM Program. Lang., 7(POPL), January 2023. doi:10.1145/3571211.
  • [26] Kevin Scott and Norman Ramsey. When do match-compilation heuristics matter. University of Virginia, Charlottesville, VA, 2000. URL: https://www.cs.tufts.edu/˜nr/cs257/archive/norman-ramsey/match.pdf.
  • [27] Titus Winters, Tom Manshreck, and Hyrum Wright. Software Engineering at Google – Lessons Learned from Programming Over Time. O’Reilly Media, Inc., 2020.
  • [28] Weixin Zhang and Bruno C. d. S. Oliveira. Pattern matching in an open world. SIGPLAN Not., 53(9):134–146, April 2020. doi:10.1145/3393934.3278124.
  • [29] Weixin Zhang, Yaozhu Sun, and Bruno C. D. S. Oliveira. Compositional programming. ACM Trans. Program. Lang. Syst., 43(3), September 2021. doi:10.1145/3460228.