4.4 Patterns as objects
4.4.2 Irrefutable patterns
An irrefutable pattern is one that always matches any object, that is, it cannot fail. Irrefutable patterns are supported in our model, in which the first step is to ask the pattern to match an object. In this respect we make a conscious departure from Newspeak’s approach: because Newspeak does what in some respects is the “correct” object-oriented thing to do by first asking the object if it wishes to be matched, Newspeak supports only unmatchable objects (objects that cannot be matched by any pattern), and not irrefutable
Client p:Pattern o:Target
p.match(o)
o.extract
MatchResult
bindings
Figure 4.1: Matching sequence in Grace. Extraction is performed only for destructuring matches.
Pattern
match(o) : MatchResult
Autozygotic TypePattern LambdaPattern
Combinator LiteralPattern Wildcard VariablePattern
OrPattern AndPattern DestructuringMatchPattern *
items pattern right
left
patterns. By definition, it is only possible to permit one of these in the same system: either there is a pattern that matches any object, or an object that is never matched by any pattern, but never both.
We made a deliberate decision to support irrefutable patterns. We did not find a use case for unmatchable objects on the user level; rather, we found it more useful to permit explicitly matching and discarding, or matching and binding, any object given.
Wildcard pattern
The simplest pattern is the wildcard pattern, which corresponds to “_” in the pattern syntax. The wildcard pattern always matches, and does not bind anything: the matched value is simply discarded. Thematchmethod immediately returns a successful match.
defwildcardPattern =object{
methodmatch(o) {
SuccessfulMatch.new(o, aTuple.new) }
}
The wildcard pattern creates no bindings and does not modify the target, and so returns aSuccessfulMatchwith an empty tuple of bindings and with the target of the match passed on unchanged.
Variable pattern
The variable pattern also always matches, but includes a single binding: the given target of the match. It applies no tests and no transformations to the object, immediately returning a successful match with the correct binding.
classVariablePattern.new(name) {
methodmatch(target) {
SuccessfulMatch.new(target, aTuple.new(target)) }
}
The variable pattern corresponds to a variable name in the pattern syntax:{ a−> a * 2 }constructs the pattern objectVariablePattern.new("a"). The target of the match is passed on unchanged, but unlike in the wildcard pattern is also used as the single binding to create from this pattern.
4.4.3
Combinators
Pattern combinators are represented by pattern objects that hold the ar- gument patterns, and use them somehow to establish their own match result.
& Combinator
The&combinator is represented by an AndPattern object. AnAndPattern conjoins two patterns, ensuring that they both match.
AndPatternis an instance of the Composite structural design pattern [56, p.163], so it is itself a Pattern. An AndPattern contains other patterns as components and uses the components recursively for matching, without knowing anything about what they are:
classAndPattern.new(pattern1, pattern2) {
methodmatch(target) {
defmatch1 = pattern1.match(target) if (!match1) then {
returnmatch1 }
defmatch2 = pattern2.match(target) if (!match2) then {
returnmatch2 }
defb = match1.bindings ++ match2.bindings SuccessfulMatch.new(target, b)
} }
Here, the component patterns are both applied to the target of the match: if either fails, theAndPatternimmediately returns the failure. When both component patterns successfully match, the AndPattern returns a SuccessfulMatchwhose bindings are the concatenation of the bindings of the component matches. The job of actually inspecting the target object or extracting bindings is left to the subsidiary patterns, and theAndPattern needs not even look at the object it has been given.
| combinator
The other obvious combinator on patterns is disjunction, represented in the syntax by|and as an object by theOrPattern, which combines two patterns, and succeeds if either one of them succeeds:
classOrPattern.new(pattern1, pattern2) {
methodmatch(o) {
if (pattern1.match(o)) then {
returnSuccessfulMatch.new(o, aTuple.new) }
if (pattern2.match(o)) then {
returnSuccessfulMatch.new(o, aTuple.new) }
FailedMatch.new(o) }
}
TheOrPatternhas a dual structure to theAndPatternand is also a com- posite, leaving the task of inspecting the target object to the component patterns, this time short-circuiting to success when a pattern matches and failing otherwise.
Unlike the AndPattern, however, the OrPatterncannot return bindings. This is because the caller cannot know which of the component patterns succeeded, and hence does not know which variables will be bound. We considered returning the intersection of the bindings from the two com- ponents, but the transformation ceases to be fully syntax-directed at that point: knowledge of the entire scope of the pattern in use is required to know what to do with each binding. We also found relatively few use cases where this was helpful, and these cases can be covered by a custom pattern if required. Providing no bindings gives a simpler implementation and explanation, and is consistent with the rule that identifiers may not be repeated in parameter lists.
4.4.4
Types
We represent Grace types by objects with a methodmatch(o)that returns a SuccessfulMatchif the argumentohas a conforming type, and aFailedMatch
otherwise. In both cases,bindingsis empty. Thus, types are also patterns. In the next example we use a type as a pattern to ensure thatohas avalue method, and then request it.
typeValuable = { value−> Number } if (Valuable.match(o)) then {
total := total + o.value }
As a consequence, types can be used as patterns in match-case expres- sions. A pattern like z : Valuable combines a type-match with a variable pattern that binds a value. This is represented using theAndPattern, so a case of the form:
case { z : Valuable−> ... z.value ... } results in the construction of the pattern
AndPattern.new(VariablePattern.new("z"), Valuable)
This pattern will succeed when the target of the match has the methods defined in theValuabletype, and will result in the variablezbeing bound to the target in the body of the block.
4.4.5
Autozygotic patterns
Numbers and Strings are patterns that match themselves, or more precisely match those objects to which they are equal: they are autozygotic, as defined in Section 4.3.3. This means that numeric and string literals within the pattern syntax have exactly their ordinary meaning: they refer to the corre- sponding Number and String objects. These objects have amatchmethod of the form:
methodmatch(o) { if (self== o) then {
returnSuccessfulMatch.new(o, aTuple.new) }
returnFailedMatch.new(o) }
User-defined objects can have the same method to make themselves autozygotic. An autozygotic object can be useful for representing a signal or enumeration value, which can then be matched against directly.
4.4.6
Destructuring patterns
A destructuring pattern extracts some of the (conceptual) state of the object it matches, and attempts to match it against other patterns. The destructur- ing pattern written in the syntax as:
Point(x, 0)
is translated into theDestructuringMatchPatternobject: DestructuringMatchPattern.new(Point,
aTuple.new(VariablePattern.new"x", 0))
This pattern combines another pattern (here,Point) which must match the object as a whole, and a tuple of other subpatterns which must match the destructured values in order for the whole pattern to match.
We will work through the implementation of this, the most complicated built-in pattern of the system, below, but the basic structure of the match is simple: first, attempt to match the target against the top-level pattern. Then obtain the destructured values from the target through itsextractmethod, and attempt to match them with the subpatterns pairwise. If any match fails, the overall match fails; otherwise, the match succeeds with all the accumulated bindings.
How does this proceed? Thematchmethod ofDestructuringMatchPattern is:
classDestructuringMatchPattern.new(pat, componentPatterns) { methodmatch(o) { defm = pat.match(o) if (!m) then { returnm }
varbindings := aTuple.new
for (componentPatterns) and (m.bindings) do { cPat, bindObj−>
definner = cPat.match(bindObj) if (!inner) then {
returnFailedMatch.new(o) }
bindings := bindings ++ inner.bindings }
SuccessfulMatch.new(o, bindings) }
}
We will go over this code in pieces.
classDestructuringMatchPattern.new(pat, componentPatterns) {
methodmatch(o) {
OurDestructuringMatchPatternconstructor has two parameters:pat, the “top-level” pattern expected to match the entire object, andcomponentPatterns,
a tuple of other patterns to match against components. Like all patterns, DestructuringMatchPatternhas amatchmethod with a single parameter.
methodmatch(o) {
defm = pat.match(o) if (!m) then {
returnm }
First we attempt to match the target,o, using the pattern we were given. We save that result intomto be used later, and then test whether it istrue (aSuccessfulMatch) orfalse(a FailedMatch). If it is not successful then the
destructuring pattern cannot succeed, so we immediately return the failure.
defm = pat.match(o) ...
varbindings := aTuple.new
for (componentPatterns) and (m.bindings) do { cPat, bindObj−>
definner = cPat.match(bindObj) if (!inner) then {
returnFailedMatch.new(o) }
Having successfully matched the top-level pattern we continue on to examine the sub-patterns. We declare a variable to hold a tuple of bindings to return, which is initially empty.
Next we want to look over two things at once: the list of sub-patterns we were given (componentPatterns) and the bindings returned from the top-level match (m.bindings). The top-level pattern determines what the extracted values from the object are, and returns them as itsbindings. The pattern may examine the object directly, ask the object to provide its con- tents, or simply fabricate some values for a particular purpose.
For each component pattern and extracted value, we try to match the pattern (cPat) against the extracted value object (bindObj). If this match fails, the entire destructuring match also fails, so we short-circuit out and return aFailedMatch.
for (componentPatterns) and (m.bindings) do { cPat, bindObj−>
definner = cPat.match(bindObj) ...
bindings := bindings ++ inner.bindings }
SuccessfulMatch.new(o, bindings)
Having successfully matched the component pattern, we move on to accumulating any bindings from it. Bindings occur most commonly from variable patterns and other destructuring patterns; in our example, the
VariablePatternforxwill create one binding. On the other hand, a type or autozygotic pattern creates no bindings — “0” will return success when matched against itself, but will not try to bind any variables. We concatenate all our accrued bindings together to be returned.
After examining all the component patterns, if we did not already return a failure then the match as a whole succeeds. We return a successful match on the given object, including the bindings we accumulated. There can be arbitrarily many bindings, depending on how many variables are used in the pattern and sub-patterns, or there can be none at all if particular components were matched directly.
Nested destructuring patterns
Destructuring patterns can be nested in the surface syntax, and exactly the same nesting manifests in the object hierarchy. Each layer of nesting constructs a newDestructuringMatchPattern, and is treated in the same way as other patterns. If a nested pattern binds many variables, these will be carried through to the result of the outermost destructuring match. The following pattern syntax:
Pair(Pair(x : Number, 1.0), p : Pair(y : String, z))
thus results in aSuccessfulMatchobject having four bindings, one for each variable named, in the left-to-right order they appear in the syntax.
4.4.7
Destructuring types
To simplify a common case we provide an extension to type patterns allow- ing them to destructure almost automatically. A type containing anextract method returning a tuple implicitly supports destructuring matching. The run-time type pattern object will implement destructuring, with the object itself being asked to provide the destructured values. Because the object conforms to the type, theextractmethod is known statically to exist.
For example, given some Point objects, a programmer may want to match those with a subset of coordinates, or extract and bind the coordi- nates. The programmer could write a pattern themselves to do so, but such a pattern would be largely repeated boilerplate. Instead they can arrange their types and objects so that it happens for them. For example, given the type: typePoint = { x−> Number y−> Number extract−> Tuple<Number,Number> }
and the class:
classaCartesianPoint.at(x' : Number, y' : Number) {
defx = x'
defy = y'
methodextract { aTuple.new(x, y) } }
we can perform a destructuring match using thePointtype pattern: match (pt)
case { p : Point(x, 0)−>"The point ({x}, 0)"} to match all points on the x axis.
To support this behaviour theDestructuringMatchPattern adds an addi- tional protocol step: in the case where the pattern itself does not provide bindings in itsMatchResult, it will examine the object for anextractmethod and substitute the return value of this method for the bindings given by the pattern. This step is necessary because type patterns in general produce no bindings —p : Pairis also a valid pattern, and it should not try to bind the internal state of the Pair object in question.