• No results found

Types and patterns

How does pattern-matching mesh with Grace’s optional, gradual type system? All of the patterns seen so far should be able to be statically typed. Most importantly, whenever a variable is bound in a match, either at the top level or via destructuring, we should be able to give it a static type. A condition required for the matching system to be integrated into the overall design of the Grace language was that the matching process should not need to descend into dynamically-typed code when the surrounding context was statically-typed; this restriction ruled out some approaches.

In this section we sketch how our approach permits types to be given to objects during matching; we will not attempt to prove correctness and we do not claim that the types assigned will be the most precise possible, simply that they are good enough to be useful, and that the type given to a variable matched by a type pattern is that type. Because these types are applied only within eventual variable bindings, it is only the ability to derive a valid static type which is important: having already matched at run-time, they are guaranteed to pass any dynamic check for the type they were given afterwards. Static type-checking in Grace is the responsibility of the dialect in use, and can be customised; we attempt only to show how sensible typings may be derived within the default structural system of the base language.

In particular, in this section we will give the types of pattern objects and theMatchResultsreturned from theirmatchmethod. We will also show how combinators affect typing, the role of destructuring matches, and show where static typing and variant types can guarantee that matches are exhaustive.

The semantics and syntax of pattern matching are independent of static types, and we seek only to provide an overview for the curious reader. A reader who is not interested in how pattern objects may be typed can proceed to Section4.6 on page 102.

4.5.1

Pattern

and

MatchResult

ThePatternandMatchResulttypes are generic, parameterised over the types of the result and the bindings:

typePattern<R,T> = {

match(o : Object)−> MatchResult<R,T> }

typeMatchResult<R,T> = { result−> R

bindings−> T }

In untyped code, these parameters are instantiated with typeUnknown, but patterns may also declare types for themselves.

The simplest pattern that assigns a type is a type pattern itself. A variable associated with a type pattern has the corresponding type, so the variablenin the patternn : Numberis given typeNumber. The pattern object would instantiate the type parameterRtoNumberin this case.

In the case:

case { p : Pair−>"Pair ({p.left}, {p.right})"}

the new variable p has static type Pair, making the operations p.left and p.rightstatically type safe. The pattern object forPairwould have the type:

typePairPattern = {

match(target : Object)−> MatchResult<Pair,Tuple<>> }

The second type parameter toMatchResultis the empty tuple because this pattern binds no variables.

Similarly, user-defined patterns define theirmatchmethod to return a result value of a particular type. This type must be correct, or the pattern code itself would have caused a type error. The result typeRcan then be given to the variable in question just as for a type pattern.

4.5.2

Destructuring

Destructuring matches may bind new variables, which are given types from the tuple type returned by the bindings method. These types in turn are derived from the types of the internal patterns written in the source, which are passed as generic type parameters to the constructor of DestructuringMatchPattern: in essence, the destructuring itself can be ignored as far as typing goes, and it behaves exactly like any other pattern match once transformed into objects. For example, given the case:

case { p : Pair(x : String, y : Number)−> ... }

the pattern generated is a variable patternpand aDestructuringMatchPattern representing thePair(...). TheMatchResult returned from that pattern has type:

typeExample = {

result−> Pair<String, Number> bindings−> Tuple<String, Number> }

defpat = ...// The DestructuringMatchPattern given above

The result type of the destructuring match, and so the type given topinside the body of the block above, isPair<String, Number>. xandywill be aString and aNumberrespectively, as expected; thebindingsmethod will return a tuple containing the values of these types to assign. TheExampletype is a subtype ofMatchResult:

defmr' : MatchResult<Pair<String, Number>, Tuple<String, Number>> = mr and the destructuring pattern itself has type:

defpat' : Pattern<Pair<String, Number>, Tuple<String, Number>>

At all times a valid type can be given to every value in use: the only place where “casting” occurs is inside the implementation of type patternsString, Number, Pair, which are built in and inherently check the types of their targets. In this case the pair’s generic parameter types match those of the bindings tuple, but in general they can differ arbitrarily.

If a type is given to a variable inside a destructuring match, that type holds outside the destructuring pattern as well. The only point where a new type is assigned is where a pattern is given on the right-hand-side of a :character.

4.5.3

Combinators

When combinators are used, the types of variables become more complex. The&combinator gives the variable the types from both patterns. In the following example,oconforms to bothXandY, so bothxandymethods may be requested:

typeX = { x } // type with x method

typeY = { y } // type with y method

match (val)

case { o : X & Y−>"Point ({o.x}, {o.y})"}

Grace’s type system uses the notationX & Yfor the type that conforms to bothXandY, so this is consistent with the base behaviour of a block, and

we can say thatohas typeX & Y. IfXandYare not types but instead general patterns, it is the intersection of their result types (R) that is important.

By contrast, the patternX | Ymatches when eitherXorYdoes. In this case, only methods that are common to objects matching eitherXorYmay be requested in statically type-safe code. The type of such an object is the untagged variant type, also writtenX | Y. All objects that have typeXand all objects that have typeYwill also have the untagged variant typeX | Y; these are exactly the objects that the patternX | Ywill match. Again, where XorYis not a type itself, it is the result typeRwhich is unioned.

4.5.4

Exhaustive matching

Untagged variant types also serve another role. A match-case expression can be statically determined to be exhaustive when the target of the match has a variant type, and all branches of the variant have associated cases. A warning can be given both for non-exhaustive matches, which may have unintended behavior, and for unreachable branches of the match:

varx : Number | String | Boolean := ... match (x)

case { n : Number−> ... } // Doesn't execute anything

case { b : Boolean−> ... }// if x is a String.

match (x)

case { n : Number | Boolean−> ... } case { s : String−> ... }

case { _−> ... }// Unreachable!

Particularly in student code, it can be useful to report errors for missed or impossible cases. The ability to do so is a natural consequence of the structure of pattern objects, when used with variant types. An instructor who wants more stringent static checking could construct a dialect that provides it, using the dialect system described in Chapter6.