Pattern Matching

Joe supports a rich pattern-matching capability inspired by the Rust language's similar capability. Joe's ~ matching operator and the var, foreach, and match statements all make use of pattern matching to do destructuring binds.

Joe supports a rich variety of patterns. Some of them can contain other patterns as subpatterns, allowing a pattern to match quite complicated data structures.

Patterns and Destructuring Binds

A destructuring bind is a way to bind one more variables to values within a complex data structure. The bind makes use of a pattern that duplicates the structure of the target value to match variable names to specific elements.

For example, suppose that the function f() returns a two-item list, and the caller wants to assign the list items to the variables x and y.

One could do this:

var result = f();
var x = result[0];
var y = result[1];

Or, one could do a destructing bind using var:

var [x, y] = f();

Here, var matches the list returned by f() against the pattern [x, y] and binds variables x and y to the matched values.

Binding Variables

We've seen binding variables in most of the examples shown above. A binding variable is a variable name that appears within the pattern and is assigned the corresponding value in the match target.

var [a, b] = [1, 2];    // a = 1, b = 2.

A binding variable can also be used to capture a subpattern. In the following example, the variable b is bound to the list [2, 3] while c and d are bound to the values of the list's items.

var list = [1, [2, 3], 4];

var [a, b@[c, d], e];

Capturing a subpattern is especially useful with foreach: the pattern can capture the entire item if the pattern matches. The following code pulls two-item lists out of a heterogeneous list of values.

foreach (item@[_, _] : inputs) { 
    println(item);
}

If a variable appears in the pattern more than once, it is bound on first appearance and the bound value must match on subsequent appearances.

// flag is true, a == 1
var flag = [a, a] ~ [1, 1];

// flag is false, a == null
var flag = [a, a] ~ [1, 2];

Wildcards

A wildcard is a pattern that matches (and ignores) any value. A wildcard is written as an identifier with leading underscore, e.g., _, _ignore, _x. For example:

var [x, _] = ["abc", "def"];

x will be assigned the value "abc", while the second item of the target list will be ignored.

It's most common to use the wildcard _; but using a longer name can be useful to document what the ignored value is:

var [first, _last] = ["Joe", "Pro"];

Using _last indicates that we don't care about the last name at the moment, but also shows that it is the last name that we are ignoring.

Constants

A constant pattern is a constant value included in the pattern; the corresponding value in the target must have that exact value.

function isPro(list) {
    if (list ~ [_, "Pro"]) {
        return true;
    } else {
        return false;
    }
}

var x = isPro(["Joe", "Pro"]);       // Returns true
var y = isPro(["Joe", "Amateur"]);   // Returns false

The constant must be a literal string, number, boolean, keyword, or null.

Interpolated Expressions

To use a computed value as a constant, interpolate it using $(...).

var a = 5;
var b = 15;

var [x, $(a + b)] = [10, 20];  // Matches; x == 10.

Here, $(a + b) evaluates to 20, which matches the second item in the target list.

The parentheses may be omitted if the interpolated expression is just a variable name:

var wanted = "Pro";

var [first, $wanted] = ["Joe", "Pro"];   

List Patterns

We've seen many list patterns in the above examples. Syntactically, a list pattern is simply a list of patterns that matches a List of values. The matched list must have exactly the same number of items as the list pattern, and each subpattern must match the corresponding item.

if (list ~ [a, [b, _], "howdy"]) {
    // ...
}

The pattern [] matches the empty list.

Sometimes the length of the list is unknown; in this case, the list pattern can provide a pattern variable to bind to the list's tail:

if (list ~ [a, b : tail]) {
    // tail gets the rest of the list.
}

The variables a and b will get list[0] and list[1], and tail will get any remaining items, or the empty list if list.size() == 2. (The match will naturally fail if list.size() < 2.)

Map Patterns

A map pattern matches objects with keys and values, e.g., Map values.

  • The keys must be constants
  • The values can be any pattern.
  • The target Map must contain all of the keys listed in the pattern, and their values must match the corresponding value patterns.
  • The target Map can contain any number of keys that don't appear in the pattern.

Some examples:

var {#a: a, #b: b} = {#a: 1, #b: 2, #c: 3};  // a = 1, b = 2
var ($x: value} = someMap;                   // value = someMap.get(x)
var {#a: [a, b, c], #b: x} = someMap;

Matching Instances with Map Patterns

A map pattern can also match any Joe value with field properties, e.g., an instance of a Joe class. The pattern's keys match the field names and the pattern's values match the field values.

  • Key patterns must be string or Keyword constants that correspond to the field names, or interpolated expressions that evaluate to such strings or keywords.
class Thing {
    method init(id, color) {
        this.id = id;
        this.color = color;
    }
}

// These two statements are equivalent
var {"id": i, "color": c} = Thing(123, "red");
var {#id: i,  #color: c}  = Thing(123, "red");

As when matching Map values, the pattern can reference a subset of the object's fields.

Named-Field Patterns

A named-field pattern matches the type and field values for any Joe value with named fields. It will also match a Fact value based on its relation and fields.

class Thing {
    method init(id, color) {
        this.id = id;
        this.color = color;
    }
}

var Thing(id: i, color: c) = Thing(123, "red");

A named-field pattern consists of the name of the desired type, followed by field-name/pattern pairs in parentheses.

  • The named type must be the target value's type or one of its supertypes.
  • The value must have all of the specified fields.
  • The field patterns must match the field values.

Types are matched based on their names, i.e., in var Thing(...) = thing; the type will match if Joe.typeOf(thing).name() == "Thing", not if Joe.typeOf(thing) == Thing. In other words, there is no requirement that the matched type is in scope; it is enough that the value being matched knows its type and that its type's name is the name included in the pattern. See Unscoped Types for more information.

Ordered-Field Patterns

Ordered-field patterns match Joe values with ordered fields, i.e., fields that can be accessed by index as well as by name. Joe records and most Fact values have ordered fields, and proxied types can have ordered fields as well. This allows a stream-lined pattern syntax.

record Person(name, age) {}

var person = Person(Joe, 80);

// These statements are identical
var Person(n, a) = person;               // Ordered-field
var Person(name: n, age: a) = person;    // Named-field

The first form matches the values of the type's fields in sequence. All fields must be represented. The field subpatterns can be any arbitrary patterns, as usual.

Values with ordered fields can also be matched by map patterns and named-field patterns.