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
- Binding Variables
- Wildcards
- Constants
- Interpolated Expressions
- List Patterns
- Map Patterns
- Matching Instances with Map Patterns
- Named-Field Patterns
- Ordered-Field Patterns
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.